diff --git a/.depcheckrc b/.depcheckrc index e9383a7985d..f4b84072048 100644 --- a/.depcheckrc +++ b/.depcheckrc @@ -14,5 +14,4 @@ ignores: [ "@yarnpkg/plugin-git", "semver", "typanion", - "turbo-ignore", ] diff --git a/.yarn/patches/@gorhom-bottom-sheet-npm-4.5.1-d8ef5d483d.patch b/.yarn/patches/@gorhom-bottom-sheet-npm-4.5.1-d8ef5d483d.patch deleted file mode 100644 index 0cc14f3652c..00000000000 --- a/.yarn/patches/@gorhom-bottom-sheet-npm-4.5.1-d8ef5d483d.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/lib/typescript/hooks/useBottomSheetDynamicSnapPoints.d.ts b/lib/typescript/hooks/useBottomSheetDynamicSnapPoints.d.ts -index 36db692d081b037cbe450ca31d186f8cc4c8410f..12d62711d3214f264bfb8bd4ce586f189dd5cece 100644 ---- a/lib/typescript/hooks/useBottomSheetDynamicSnapPoints.d.ts -+++ b/lib/typescript/hooks/useBottomSheetDynamicSnapPoints.d.ts -@@ -14,9 +14,7 @@ - * @deprecated will be deprecated in the next major release! please use the new introduce prop `enableDynamicSizing`. - */ - export declare const useBottomSheetDynamicSnapPoints: (initialSnapPoints: Array) => { -- animatedSnapPoints: Readonly<{ -- value: (string | number)[]; -- }>; -+ animatedSnapPoints: import("react-native-reanimated").SharedValue<(string | number)[]>; - animatedHandleHeight: import("react-native-reanimated").SharedValue; - animatedContentHeight: import("react-native-reanimated").SharedValue; - handleContentLayout: ({ nativeEvent: { layout: { height }, }, }: { diff --git a/.yarn/patches/react-native-wagmi-charts-npm-2.3.0-8e836a8f3c.patch b/.yarn/patches/react-native-wagmi-charts-npm-2.3.0-8e836a8f3c.patch index ffce467dbdd..bc7e38bdc42 100644 --- a/.yarn/patches/react-native-wagmi-charts-npm-2.3.0-8e836a8f3c.patch +++ b/.yarn/patches/react-native-wagmi-charts-npm-2.3.0-8e836a8f3c.patch @@ -1,26 +1,5 @@ -diff --git a/lib/typescript/src/charts/line/useDatetime.d.ts b/lib/typescript/src/charts/line/useDatetime.d.ts -index c6f73dd8b31294d0e4c0597519dd998ccd84ad30..9f9eb03e25d1e020de37c706e8f8803d0b9dcefa 100644 ---- a/lib/typescript/src/charts/line/useDatetime.d.ts -+++ b/lib/typescript/src/charts/line/useDatetime.d.ts -@@ -1,13 +1,10 @@ - import type { TFormatterFn } from '../candle/types'; -+import { SharedValue } from 'react-native-reanimated'; - export declare function useLineChartDatetime({ format, locale, options, }?: { - format?: TFormatterFn; - locale?: string; - options?: Intl.DateTimeFormatOptions; - }): { -- value: Readonly<{ -- value: string; -- }>; -- formatted: Readonly<{ -- value: string; -- }>; -+ value: SharedValue; -+ formatted: SharedValue; - }; diff --git a/src/charts/line/ChartPath.tsx b/src/charts/line/ChartPath.tsx -index 3807c185c9456d2976c305df94574ff7d948b32a..7b49281318da294617ec25c1a2c04d3e231bb298 100644 +index 3807c185c9456d2976c305df94574ff7d948b32a..5cf985422cf49120f943c98084b08c16faf452d9 100644 --- a/src/charts/line/ChartPath.tsx +++ b/src/charts/line/ChartPath.tsx @@ -18,7 +18,6 @@ const BACKGROUND_COMPONENTS = [ @@ -31,12 +10,13 @@ index 3807c185c9456d2976c305df94574ff7d948b32a..7b49281318da294617ec25c1a2c04d3e 'LineChartTooltip', ]; const FOREGROUND_COMPONENTS = ['LineChartHighlight', 'LineChartDot']; -@@ -166,6 +165,18 @@ export function LineChartPathWrapper({ +@@ -166,10 +165,25 @@ export function LineChartPathWrapper({ + + ++ + + -diff --git a/src/charts/line/Dot.tsx b/src/charts/line/Dot.tsx -index dd49d3e49231a5e4f56138bbf3ec51013515f7b0..dfdaa349e9a25dca297234120cc6bd9f5915ed0d 100644 ---- a/src/charts/line/Dot.tsx -+++ b/src/charts/line/Dot.tsx -@@ -2,13 +2,12 @@ import * as React from 'react'; - import Animated, { - Easing, - useAnimatedProps, -- useDerivedValue, - withRepeat, - withSequence, - withTiming, - } from 'react-native-reanimated'; --import { Circle, CircleProps } from 'react-native-svg'; - import { getYForX } from 'react-native-redash'; -+import { Circle, CircleProps } from 'react-native-svg'; - - import { LineChartDimensionsContext } from './Chart'; - import { LineChartPathContext } from './LineChartPathContext'; -@@ -72,29 +71,13 @@ export function LineChartDot({ - - //////////////////////////////////////////////////////////// - -- const x = useDerivedValue( -- () => withTiming(pointWidth * at), -- [at, pointWidth] -- ); -- const y = useDerivedValue( -- () => withTiming(getYForX(parsedPath!, x.value) || 0), -- [parsedPath, x] -- ); -+ const x = pointWidth * at; -+ const y = getYForX(parsedPath!, x) ?? 0; - - //////////////////////////////////////////////////////////// - -- const animatedDotProps = useAnimatedProps( -- () => ({ -- cx: x.value, -- cy: y.value, -- }), -- [x, y] -- ); -- - const animatedOuterDotProps = useAnimatedProps(() => { - let defaultProps = { -- cx: x.value, -- cy: y.value, - opacity: 0.1, - r: outerSize, - }; -@@ -113,25 +96,27 @@ export function LineChartDot({ - const easing = Easing.out(Easing.sin); - const animatedOpacity = withRepeat( - withSequence( -- withTiming(0.8), -+ withTiming(0.8, { -+ duration: 0, -+ }), - withTiming(0, { - duration: pulseDurationMs, - easing, - }) - ), -- -1, -- false -+ -1 - ); - const scale = withRepeat( - withSequence( -- withTiming(0), -+ withTiming(0, { -+ duration: 0, -+ }), - withTiming(outerSize, { - duration: pulseDurationMs, - easing, - }) - ), -- -1, -- false -+ -1 - ); - - if (pulseBehaviour === 'while-inactive') { -@@ -146,15 +131,16 @@ export function LineChartDot({ - opacity: animatedOpacity, - r: scale, - }; -- }, [hasPulse, isActive, outerSize, pulseBehaviour, pulseDurationMs, x, y]); -+ }, [hasPulse, isActive, outerSize, pulseBehaviour, pulseDurationMs]); - - //////////////////////////////////////////////////////////// - - return ( - <> - - )} ++ + ++ + + ); + } diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index f70773659eb..00000000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @uniswap/web-admins diff --git a/RELEASE b/RELEASE index b39db7f46e6..807bd93c566 100644 --- a/RELEASE +++ b/RELEASE @@ -1,24 +1,14 @@ -IPFS hash of the deployment: -- CIDv0: `QmciGvhbiSVjSu6u9JwRzFp5uBRnawnd5M8NMk8d9XNACT` -- CIDv1: `bafybeigvrxb4vjhzc3k7xqtwa4dijoev545kpjmo4anikgsbckku45k4qq` +Another week, another update. Check out whats new below: -The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). +Settings Polish — We switched spam and low balance settings to universal settings. In addition, we added an opt out for anonymous analytics within the app. -You can also access the Uniswap Interface from an IPFS gateway. -**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. -**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org). -Your Uniswap settings are never remembered across different URLs. - -IPFS gateways: -- https://bafybeigvrxb4vjhzc3k7xqtwa4dijoev545kpjmo4anikgsbckku45k4qq.ipfs.dweb.link/ -- https://bafybeigvrxb4vjhzc3k7xqtwa4dijoev545kpjmo4anikgsbckku45k4qq.ipfs.cf-ipfs.com/ -- [ipfs://QmciGvhbiSVjSu6u9JwRzFp5uBRnawnd5M8NMk8d9XNACT/](ipfs://QmciGvhbiSVjSu6u9JwRzFp5uBRnawnd5M8NMk8d9XNACT/) - -## 5.8.0 (2024-02-09) - - -### Features - -* **web:** outage banner for arbitrum, optimism, polygon (#6218) e57ca63 +Quick Copy Contract Addresses — We added the ability quickly copy any token address! Simply press and hold on any token to bring up the option to swap or copy a contract address. +Other notable changes: +- View-only wallet polish +- Activity bug fixes +- Receive QR polish +- Import wallet polish +- Token details page bug fixes +- Updated treatment for tokens with no logos diff --git a/VERSION b/VERSION index cec32518076..bfe1c075ef5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -web/5.8.0 \ No newline at end of file +mobile/1.19.2 \ No newline at end of file diff --git a/apps/mobile/.depcheckrc b/apps/mobile/.depcheckrc index 55aadbc01ae..70278cd3b92 100644 --- a/apps/mobile/.depcheckrc +++ b/apps/mobile/.depcheckrc @@ -19,6 +19,7 @@ ignores: [ "src", "ui", "tsconfig", + "eslint-config-custom", ## Subpackages of installed packages "@redux-saga/core", "@ethersproject/constants", diff --git a/apps/mobile/.eslintrc.js b/apps/mobile/.eslintrc.js index a8f1fdf1029..ee1952fe87e 100644 --- a/apps/mobile/.eslintrc.js +++ b/apps/mobile/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { root: true, - extends: ['@uniswap/eslint-config/native'], + extends: ['custom'], parserOptions: { project: 'tsconfig.json', tsconfigRootDir: __dirname, diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 83278c886b4..974e42e49d0 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -125,17 +125,17 @@ android { dev { isDefault(true) applicationIdSuffix ".dev" - versionName "1.21" + versionName "1.19.2" dimension "variant" } beta { applicationIdSuffix ".beta" - versionName "1.21" + versionName "1.19.2" dimension "variant" } prod { dimension "variant" - versionName "1.21" + versionName "1.19.2" } } diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 98a145ca799..eb4dc689106 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ - { diff --git a/apps/mobile/e2e/usecases/ImportAccounts.js b/apps/mobile/e2e/usecases/ImportAccounts.js index 059f77ad4a5..cec1cdb1777 100644 --- a/apps/mobile/e2e/usecases/ImportAccounts.js +++ b/apps/mobile/e2e/usecases/ImportAccounts.js @@ -1,7 +1,7 @@ import { by, device, element, expect } from 'detox' import { Accounts } from 'src/e2e/utils/fixtures' +import { ElementName } from 'src/features/telemetry/constants' import { sleep } from 'utilities/src/time/timing' -import { ElementName } from 'wallet/src/telemetry/constants' export function ImportAccounts() { it('creates a readonly account', async () => { diff --git a/apps/mobile/e2e/usecases/Swap.js b/apps/mobile/e2e/usecases/Swap.js index d9eaeb77235..77a905d1248 100644 --- a/apps/mobile/e2e/usecases/Swap.js +++ b/apps/mobile/e2e/usecases/Swap.js @@ -1,5 +1,5 @@ import { by, device, element, expect } from 'detox' -import { ElementName } from 'wallet/src/telemetry/constants' +import { ElementName } from 'src/features/telemetry/constants' export function Swap() { it('saves the original amount on usd toggle', async () => { diff --git a/apps/mobile/e2e/utils/utils.js b/apps/mobile/e2e/utils/utils.js index 8a0cfe317e4..14da6e0e17f 100644 --- a/apps/mobile/e2e/utils/utils.js +++ b/apps/mobile/e2e/utils/utils.js @@ -1,7 +1,7 @@ import { by, device, element } from 'detox' import { Accounts } from 'src/e2e/utils/fixtures' +import { ElementName } from 'src/features/telemetry/constants' import { sleep } from 'utilities/src/time/timing' -import { ElementName } from 'wallet/src/telemetry/constants' /** Opens Account page and imports a managed account */ export async function quickOnboarding() { diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile index 5a14f7ce24a..12023c4fe3c 100644 --- a/apps/mobile/ios/Podfile +++ b/apps/mobile/ios/Podfile @@ -50,6 +50,10 @@ target 'Uniswap' do installer.pods_project.targets.each do |target| target.build_configurations.each do |config| + if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < '9.0'.to_f + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0' + end + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'No' end diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 635c2a14b09..03c7bdfa574 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -1282,10 +1282,35 @@ PODS: - React-Core - RNPermissions (3.6.0): - React-Core - - RNReanimated (3.6.2): - - RCT-Folly (= 2021.07.22.00) + - RNReanimated (3.3.0): + - DoubleConversion + - FBLazyVector + - FBReactNativeSpec + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-callinvoker - React-Core + - React-Core/DevSupport + - React-Core/RCTWebSocket + - React-CoreModules + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-RCTActionSheet + - React-RCTAnimation + - React-RCTBlob + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTSettings + - React-RCTText - ReactCommon/turbomodule/core + - Yoga - RNScreens (3.24.0): - React-Core - React-RCTImage @@ -1635,7 +1660,7 @@ SPEC CHECKSUMS: Apollo: fe380f40e55e501a2499dd5885fab0cdf082b2bb AppsFlyerFramework: 88a6eed37ad52bcee4ad74232efa8e22809d06c9 Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b - boost: 0a937fbcfdd646fca221c4f1d9750d7ccfdfc2dc + boost: 57d2868c099736d80fcd648bf211b4431e51a558 BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33 Burnt: 708556f6283e1b81767e6642e088819d85d1ea08 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 @@ -1740,7 +1765,7 @@ SPEC CHECKSUMS: RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 RNPermissions: de7b7c3fe1680d974ac7a85e3e97aa539c0e68ea - RNReanimated: 807546b6fc06d978ef0edc0ebc54db5741c6e432 + RNReanimated: d6b4b867b6d1ee0798f5fb372708fa4bb8d66029 RNScreens: b21dc57dfa2b710c30ec600786a3fc223b1b92e7 RNSentry: 4fb2cd7d2d6cb94423c24884488206ef881da136 RNSVG: c1e76b81c76cdcd34b4e1188852892dc280eb902 @@ -1754,6 +1779,6 @@ SPEC CHECKSUMS: Yoga: 135109c9b8c5d1a8af3a58d21cd4c7aa7f3bf555 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 22ab4b87e0bceeaa9e245c485c36fca595139568 +PODFILE CHECKSUM: 632909767e5e0022317f148f858c5b6f587e5900 COCOAPODS: 1.14.3 diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj index 8e7a7f16f4e..455c55e5ae2 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -140,9 +140,6 @@ 681301B22A3726EE00A5BF43 /* pending_send.riv in Resources */ = {isa = PBXBuildFile; fileRef = 681301AE2A3726EE00A5BF43 /* pending_send.riv */; }; 681301B32A3726EE00A5BF43 /* onboarding_light.riv in Resources */ = {isa = PBXBuildFile; fileRef = 681301AF2A3726EE00A5BF43 /* onboarding_light.riv */; }; 681301B42A3726EE00A5BF43 /* pending_swap.riv in Resources */ = {isa = PBXBuildFile; fileRef = 681301B02A3726EE00A5BF43 /* pending_swap.riv */; }; - 6BC7D07E2B5FF02400617C95 /* ScantasticEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */; }; - 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */; }; - 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */; }; 6C8EFC2D2891B99100FBD8EB /* EncryptionHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8EFC2C2891B99100FBD8EB /* EncryptionHelperTests.swift */; }; 6CA91BDB2A95223C00C4063E /* RNEthersRS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BD92A95223C00C4063E /* RNEthersRS.swift */; }; 6CA91BDC2A95223C00C4063E /* RnEthersRS.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDA2A95223C00C4063E /* RnEthersRS.m */; }; @@ -439,9 +436,6 @@ 681301AF2A3726EE00A5BF43 /* onboarding_light.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = onboarding_light.riv; sourceTree = ""; }; 681301B02A3726EE00A5BF43 /* pending_swap.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = pending_swap.riv; sourceTree = ""; }; 68FD07BE7700B63D569EB256 /* Pods-WidgetIntentExtension.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.dev.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.dev.xcconfig"; sourceTree = ""; }; - 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScantasticEncryption.m; sourceTree = ""; }; - 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScantasticEncryption.swift; sourceTree = ""; }; - 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionUtils.swift; sourceTree = ""; }; 6C8EFC2C2891B99100FBD8EB /* EncryptionHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionHelperTests.swift; sourceTree = ""; }; 6CA91BD82A95223C00C4063E /* RNEthersRS-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RNEthersRS-Bridging-Header.h"; sourceTree = ""; }; 6CA91BD92A95223C00C4063E /* RNEthersRS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNEthersRS.swift; sourceTree = ""; }; @@ -940,21 +934,9 @@ name = ExpoModulesProviders; sourceTree = ""; }; - 6BC7D07A2B5FF02400617C95 /* Scantastic */ = { - isa = PBXGroup; - children = ( - 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */, - 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */, - 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */, - ); - name = Scantastic; - path = Uniswap/Onboarding/Scantastic; - sourceTree = ""; - }; 6C84F055283D83CF0071FA2E /* Onboarding */ = { isa = PBXGroup; children = ( - 6BC7D07A2B5FF02400617C95 /* Scantastic */, 8E89C3A52AB8AAA400C84DE5 /* Backup */, 8EA8AB2F2AB7ED3C004E7EF3 /* Import */, ); @@ -1923,7 +1905,6 @@ FD7304D028A3650A0085BDEA /* Colors.swift in Sources */, 8E89C3AF2AB8AAA400C84DE5 /* MnemonicDisplayView.swift in Sources */, 9FEC9B8B2A858CF1003CD019 /* AppDelegate.m in Sources */, - 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */, 8EA8AB3B2AB7ED3C004E7EF3 /* SeedPhraseInputManager.m in Sources */, 6CA91BDB2A95223C00C4063E /* RNEthersRS.swift in Sources */, 8EA8AB3C2AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift in Sources */, @@ -1931,7 +1912,6 @@ 8E89C3B12AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift in Sources */, 07B0676D2A7D6EC8001DD9B9 /* RNWidgets.m in Sources */, 8EBFB1552ABA6AA6006B32A8 /* PasteIcon.swift in Sources */, - 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */, 8E89C3B02AB8AAA400C84DE5 /* MnemonicWordView.swift in Sources */, 07B0676C2A7D6EC8001DD9B9 /* RNWidgets.swift in Sources */, 8E89C3AE2AB8AAA400C84DE5 /* MnemonicConfirmationView.swift in Sources */, @@ -1944,7 +1924,6 @@ 9FCEBF012A95A8E00079EDDB /* RNWalletConnect.swift in Sources */, 6CA91BDC2A95223C00C4063E /* RnEthersRS.m in Sources */, 6CA91BE12A95226200C4063E /* RNCloudStorageBackupsManager.m in Sources */, - 6BC7D07E2B5FF02400617C95 /* ScantasticEncryption.m in Sources */, A3F0A5B1272B1DFA00895B25 /* KeychainSwiftDistrib.swift in Sources */, 8E89C3B42AB8AAA400C84DE5 /* MnemonicConfirmationManager.m in Sources */, 1440B371A1C9A42F3E91DAAE /* ExpoModulesProvider.swift in Sources */, @@ -2450,7 +2429,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2496,7 +2475,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; @@ -2542,7 +2521,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; @@ -2588,7 +2567,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; @@ -2630,7 +2609,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2673,7 +2652,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; @@ -2716,7 +2695,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; @@ -2759,7 +2738,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; @@ -2795,7 +2774,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2833,7 +2812,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3003,7 +2982,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3047,7 +3026,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; @@ -3143,7 +3122,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3214,7 +3193,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; @@ -3310,7 +3289,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3381,7 +3360,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.21; + MARKETING_VERSION = 1.19.2; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; diff --git a/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme b/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme index c5e27e3a5eb..0d6918c5b84 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme +++ b/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme @@ -1,6 +1,6 @@ String { - var base64 = base64url - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - if base64.count % 4 != 0 { - base64.append(String(repeating: "=", count: 4 - base64.count % 4)) - } - return base64 -} - -// Calculate the length field for an ASN.1 sequence. -func lengthField(of valueField: [UInt8]) throws -> [UInt8] { - var count = valueField.count - - if count < 128 { - return [ UInt8(count) ] - } - - // The number of bytes needed to encode count. - let lengthBytesCount = Int((log2(Double(count)) / 8) + 1) - - // The first byte in the length field encoding the number of remaining bytes. - let firstLengthFieldByte = UInt8(128 + lengthBytesCount) - - var lengthField: [UInt8] = [] - for _ in 0..> 8 - } - - // Include the first byte. - lengthField.insert(firstLengthFieldByte, at: 0) - - return lengthField -} - -func generatePublicRSAKey(modulus: String, exponent: String) throws -> SecKey { - // Lets encode them from b64 url to b64 - let encodedModulus = Base64URLToBase64(base64url: modulus) - let encodedExponent = Base64URLToBase64(base64url: exponent) - - // First we need to get our modulus and exponent into UInt8 arrays - // We can do this by decoding the Base64 strings (URL safe) into Data - // and then converting the Data into UInt8 arrays - guard let modulusData = Data(base64Encoded: encodedModulus) else { - throw EncryptionError.invalidModulus - } - - guard let exponentData = Data(base64Encoded: encodedExponent) else { - throw EncryptionError.invalidExponent - } - - var modulus = modulusData.withUnsafeBytes { Data(Array($0)).withUnsafeBytes { Array($0) } } - let exponent = exponentData.withUnsafeBytes { Data(Array($0)).withUnsafeBytes { Array($0) } } - - // Lets add 0x00 at the front of the modulus - modulus.insert(0x00, at: 0) - - var sequenceEncoded: [UInt8] = [] - do { - // encode as integers - var modulusEncoded: [UInt8] = [] - modulusEncoded.append(0x02) - modulusEncoded.append(contentsOf: try lengthField(of: modulus)) - modulusEncoded.append(contentsOf: modulus) - - var exponentEncoded: [UInt8] = [] - exponentEncoded.append(0x02) - exponentEncoded.append(contentsOf: try lengthField(of: exponent)) - exponentEncoded.append(contentsOf: exponent) - - sequenceEncoded.append(0x30) - sequenceEncoded.append(contentsOf: try lengthField(of: (modulusEncoded + exponentEncoded))) - sequenceEncoded.append(contentsOf: (modulusEncoded + exponentEncoded)) - } catch { - throw EncryptionError.invalidPublicKey - } - - let keyData = Data(sequenceEncoded) - - // RSA key size is the number of bits of the modulus. - let keySize = (modulus.count * 8) - - let attributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeyClass as String: kSecAttrKeyClassPublic, - kSecAttrKeySizeInBits as String: keySize - ] - - guard let publicKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, nil) else { - throw EncryptionError.invalidPublicKey - } - - return publicKey -} - -func encryptForStorage(plaintext: String, publicKey: SecKey) throws -> Data -{ - // Encrypt the plaintext - let plaintextData = Data(plaintext.utf8) - let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256 - - guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else { - throw EncryptionError.invalidPublicKey - } - - var error: Unmanaged? - guard let ciphertextData = SecKeyCreateEncryptedData(publicKey, algorithm, plaintextData as CFData, &error) else { - if let error = error { - throw error.takeRetainedValue() as Error - } else { - throw EncryptionError.unknown - } - } - - return ciphertextData as Data -} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.m b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.m deleted file mode 100644 index 2612b52312a..00000000000 --- a/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.m +++ /dev/null @@ -1,19 +0,0 @@ -// -// ScantasticEncryption.m -// Uniswap -// -// Created by Christine Legge on 1/23/24. -// - -#import -#import - -@interface RCT_EXTERN_MODULE(ScantasticEncryption, RCTEventEmitter) - -RCT_EXTERN_METHOD(getEncryptedMnemonic: (NSString *)mnemonicId - n: (NSString *)n - e: (NSString *)e - resolve: (RCTPromiseResolveBlock)resolve - reject: (RCTPromiseRejectBlock)reject) - -@end diff --git a/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.swift b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.swift deleted file mode 100644 index edabb1f32cd..00000000000 --- a/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// ScantasticEncryption.swift -// Uniswap -// -// Created by Christine Legge on 1/23/24. -// - -import Foundation -import CryptoKit - -enum ScantasticError: String, Error { - case publicKeyError = "publicKeyError" - case cipherTextError = "cipherTextError" -} - -@objc(ScantasticEncryption) -class ScantasticEncryption: RCTEventEmitter { - let rnEthersRS = RNEthersRS() - - @objc override static func requiresMainQueueSetup() -> Bool { - return false - } - - override func supportedEvents() -> [String]! { - return [] - } - - /** - Retrieves encrypted mnemonic - - - parameter mnemonicId: key string associated with mnemonic to backup - - parameter n: base64encoded value - - parameter e: base64encoded value - */ - @objc(getEncryptedMnemonic:n:e:resolve:reject:) - func getEncryptedMnemonic( - mnemonicId: String, n: String, e: String, resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - - guard let mnemonic = rnEthersRS.retrieveMnemonic(mnemonicId: mnemonicId) else { - return reject(RNEthersRSError.retrieveMnemonicError.rawValue, "Failed to retrieve mnemonic", RNEthersRSError.retrieveMnemonicError) - } - - let publicKey: SecKey - do { - publicKey = try generatePublicRSAKey(modulus: n, exponent: e) - } catch { - return reject(ScantasticError.publicKeyError.rawValue, "Failed to generate public Key ", ScantasticError.publicKeyError) - } - - let encodedCiphertext: Data - do { - encodedCiphertext = try encryptForStorage(plaintext:mnemonic,publicKey:publicKey) - } catch { - return reject(ScantasticError.cipherTextError.rawValue, "Failed to encrypt the mnemonic", ScantasticError.cipherTextError) - } - - let b64encodedCiphertext = encodedCiphertext.base64EncodedString() - return resolve(b64encodedCiphertext) - } -} diff --git a/apps/mobile/ios/apollo-codegen-config.json b/apps/mobile/ios/apollo-codegen-config.json index 33d9500677f..3c001d4bdd3 100644 --- a/apps/mobile/ios/apollo-codegen-config.json +++ b/apps/mobile/ios/apollo-codegen-config.json @@ -1,32 +1,34 @@ { - "schemaNamespace": "MobileSchema", - "options": { - "cocoapodsCompatibleImportStatements": true + "schemaNamespace" : "MobileSchema", + "options" : { + "cocoapodsCompatibleImportStatements" : true }, - "input": { - "operationSearchPaths": [ + "input" : { + "operationSearchPaths" : [ "../../../apps/mobile/src/components/PriceExplorer/TokenPriceHistory.graphql", "../../../apps/mobile/src/components/explore/search/SearchPopularTokens.graphql", "../../../packages/wallet/src/data/queries.graphql" ], - "schemaSearchPaths": [ - "../../../packages/wallet/src/data/schema.graphql" + "schemaSearchPaths" : [ + "../../../packages/wallet/src/data/__generated__/schema.graphql" ] }, - "output": { - "testMocks": { - "none": {} + "output" : { + "testMocks" : { + "none" : { + } }, - "schemaTypes": { - "path": "./WidgetsCore/MobileSchema", - "moduleType": { - "embeddedInTarget": { - "name": "WidgetsCore" + "schemaTypes" : { + "path" : "./WidgetsCore/MobileSchema", + "moduleType" : { + "embeddedInTarget" : { + "name" : "WidgetsCore" } } }, - "operations": { - "inSchemaModule": {} + "operations" : { + "inSchemaModule" : { + } } } } diff --git a/apps/mobile/jest-setup.js b/apps/mobile/jest-setup.js index 2f566d98f4e..c323dbe131b 100644 --- a/apps/mobile/jest-setup.js +++ b/apps/mobile/jest-setup.js @@ -4,7 +4,6 @@ import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js' import 'core-js' // necessary so setImmediate works in tests import { localizeMock as mockRNLocalize } from 'react-native-localize/mock' -import { AppearanceSettingType } from 'wallet/src/features/appearance/slice' import { MockLocalizationContext } from 'wallet/src/test/utils' // avoids polluting console in test runs, while keeping important log levels @@ -118,16 +117,3 @@ jest.mock('react-i18next', () => ({ init: jest.fn(), }, })) - -// Mock the appearance hook for all tests -const mockAppearanceSetting = AppearanceSettingType.System -jest.mock('wallet/src/features/appearance/hooks', () => { - return { - useCurrentAppearanceSetting: () => mockAppearanceSetting, - } -}) -jest.mock('wallet/src/features/appearance/hooks', () => { - return { - useSelectedColorScheme: () => 'light', - } -}) diff --git a/apps/mobile/jest.config.js b/apps/mobile/jest.config.js index 285f047fb4a..a2ff060e49e 100644 --- a/apps/mobile/jest.config.js +++ b/apps/mobile/jest.config.js @@ -1,6 +1,5 @@ const preset = require('../../config/jest-presets/jest/jest-preset') -/** @type {import('jest').Config} */ module.exports = { ...preset, preset: 'jest-expo', diff --git a/apps/mobile/package.json b/apps/mobile/package.json index f07964409a2..a6e3f1eef40 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -25,7 +25,7 @@ "link:assets": "react-native-asset", "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", "hardhat": "hardhat node", - "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 8", + "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 6", "ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios", "ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift", "ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"", @@ -77,14 +77,16 @@ "@uniswap/analytics": "1.7.0", "@uniswap/analytics-events": "2.29.0", "@uniswap/ethers-rs-mobile": "0.0.5", + "@uniswap/permit2-sdk": "1.2.0", + "@uniswap/router-sdk": "1.7.1", "@uniswap/sdk-core": "4.0.7", - "@uniswap/v3-sdk": "3.10.2", + "@uniswap/universal-router-sdk": "1.5.8", + "@uniswap/v3-sdk": "3.10.0", "@walletconnect/core": "2.10.1", "@walletconnect/react-native-compat": "2.10.1", "@walletconnect/utils": "2.10.1", "@walletconnect/web3wallet": "1.9.1", "apollo3-cache-persist": "0.14.1", - "axios": "1.6.5", "babel-plugin-transform-inline-environment-variables": "0.4.4", "babel-plugin-transform-remove-console": "6.9.4", "cross-fetch": "3.1.5", @@ -94,6 +96,7 @@ "expo-av": "13.4.1", "expo-barcode-scanner": "12.7.0", "expo-blur": "12.2.2", + "expo-clipboard": "4.1.2", "expo-haptics": "12.0.1", "expo-linear-gradient": "12.3.0", "expo-linking": "4.0.1", @@ -102,7 +105,9 @@ "expo-modules-core": "1.5.8", "expo-screen-capture": "4.2.0", "expo-store-review": "~6.2.1", + "expo-web-browser": "12.0.0", "fuse.js": "6.5.3", + "jsbi": "3.2.5", "lodash": "4.17.21", "no-yolo-signatures": "0.0.2", "qrcode": "1.5.1", @@ -124,7 +129,7 @@ "react-native-onesignal": "4.5.2", "react-native-pager-view": "6.0.1", "react-native-permissions": "3.6.0", - "react-native-reanimated": "3.6.2", + "react-native-reanimated": "3.3.0", "react-native-restart": "0.0.27", "react-native-safe-area-context": "4.5.0", "react-native-screens": "3.24.0", @@ -147,7 +152,7 @@ "wallet": "workspace:^" }, "devDependencies": { - "@babel/core": "^7.20.5", + "@babel/core": "7.12.9", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-proposal-logical-assignment-operators": "7.16.7", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", @@ -160,7 +165,6 @@ "@testing-library/react-native": "11.5.0", "@types/react-native": "0.71.3", "@types/redux-mock-store": "1.0.6", - "@uniswap/eslint-config": "workspace:^", "@walletconnect/types": "2.8.6", "@welldone-software/why-did-you-render": "7.0.1", "babel-jest": "29.6.1", @@ -191,7 +195,7 @@ "react-test-renderer": "18.2.0", "redux-flipper": "2.0.2", "redux-saga-test-plan": "4.0.4", - "typescript": "5.3.3", + "typescript": "4.9.4", "yarn-deduplicate": "6.0.0" }, "expo": { diff --git a/apps/mobile/scripts/podinstall.sh b/apps/mobile/scripts/podinstall.sh index e6b5f42feaa..fccd389c4f5 100755 --- a/apps/mobile/scripts/podinstall.sh +++ b/apps/mobile/scripts/podinstall.sh @@ -1,2 +1,2 @@ #!/bin/bash -cd ios/ && bundle install && bundle exec pod install && cd .. +cd ios/ && bundle exec pod install && cd .. diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index 1aba9b58c46..d379daa3479 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -2,7 +2,7 @@ import { ApolloProvider } from '@apollo/client' import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import * as Sentry from '@sentry/react-native' import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance' -import { PropsWithChildren, default as React, StrictMode, useCallback, useEffect } from 'react' +import { default as React, PropsWithChildren, StrictMode, useCallback, useEffect } from 'react' import { NativeModules, StatusBar } from 'react-native' import appsFlyer from 'react-native-appsflyer' import { getUniqueId } from 'react-native-device-info' @@ -11,16 +11,15 @@ import { SafeAreaProvider } from 'react-native-safe-area-context' import { enableFreeze } from 'react-native-screens' import { PersistGate } from 'redux-persist/integration/react' import { ErrorBoundary } from 'src/app/ErrorBoundary' -import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { AppModals } from 'src/app/modals/AppModals' -import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { useIsPartOfNavigationTree } from 'src/app/navigation/hooks' import { AppStackNavigator } from 'src/app/navigation/navigation' +import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { persistor, store } from 'src/app/store' +import { OfflineBanner } from 'src/components/banners/OfflineBanner' import Trace from 'src/components/Trace/Trace' import { TraceUserProperties } from 'src/components/Trace/TraceUserProperties' -import { OfflineBanner } from 'src/components/banners/OfflineBanner' import { usePersistedApolloClient } from 'src/data/usePersistedApolloClient' import { initAppsFlyer } from 'src/features/analytics/appsflyer' import { LockScreenContextProvider } from 'src/features/authentication/lockScreenContext' @@ -31,6 +30,7 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { MobileEventName } from 'src/features/telemetry/constants' import { shouldLogScreen } from 'src/features/telemetry/directLogScreens' import { selectAllowAnalytics } from 'src/features/telemetry/selectors' +import { TransactionHistoryUpdater } from 'src/features/transactions/TransactionHistoryUpdater' import { processWidgetEvents, setAccountAddressesUserDefaults, @@ -40,22 +40,20 @@ import { import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' import { getSentryEnvironment, getStatsigEnvironmentTier } from 'src/utils/version' import { Statsig, StatsigProvider } from 'statsig-react-native' -import { flexStyles, useIsDarkMode } from 'ui/src' +import { flexStyles } from 'ui/src' import { registerConsoleOverrides } from 'utilities/src/logger/console' import { logger } from 'utilities/src/logger/logger' import { useAsyncData } from 'utilities/src/react/hooks' import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext' import { config } from 'wallet/src/config' import { uniswapUrls } from 'wallet/src/constants/urls' -import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' +import { useCurrentAppearanceSetting, useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { EXPERIMENT_NAMES, FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' -import { LocalizationContextProvider } from 'wallet/src/features/language/LocalizationContext' import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' +import { LocalizationContextProvider } from 'wallet/src/features/language/LocalizationContext' import { updateLanguage } from 'wallet/src/features/language/slice' -import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater' -import { UnitagUpdaterContextProvider } from 'wallet/src/features/unitags/context' import { Account } from 'wallet/src/features/wallet/accounts/types' import { WalletContextProvider } from 'wallet/src/features/wallet/context' import { useAccounts } from 'wallet/src/features/wallet/hooks' @@ -187,29 +185,25 @@ function AppOuter(): JSX.Element | null { - - - - - - { - routingInstrumentation.registerNavigationContainer(navigationRef) - }}> - - - - - - - - - - - - - - + + + + + { + routingInstrumentation.registerNavigationContainer(navigationRef) + }}> + + + + + + + + + + + diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx deleted file mode 100644 index 5fa97542348..00000000000 --- a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { PropsWithChildren, useCallback } from 'react' -import { useAppDispatch } from 'src/app/hooks' -import { useAppStackNavigation } from 'src/app/navigation/types' -import { closeModal, openModal } from 'src/features/modals/modalSlice' -import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' -import { Screens } from 'src/screens/Screens' -import { - NavigateToSwapFlowArgs, - WalletNavigationProvider, -} from 'wallet/src/contexts/WalletNavigationContext' -import { ModalName } from 'wallet/src/telemetry/constants' - -export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element { - const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens) - const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity) - const navigateToSwapFlow = useNavigateToSwapFlow() - const navigateToTokenDetails = useNavigateToTokenDetails() - - return ( - - {children} - - ) -} - -function useNavigateToHomepageTab(tab: HomeScreenTabIndex): () => void { - const { navigate } = useAppStackNavigation() - - return useCallback((): void => { - navigate(Screens.Home, { tab }) - }, [navigate, tab]) -} - -function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { - const dispatch = useAppDispatch() - - return useCallback( - (args: NavigateToSwapFlowArgs): void => { - const initialState = args?.initialState - - dispatch(closeModal({ name: ModalName.Swap })) - dispatch(openModal({ name: ModalName.Swap, initialState })) - }, - [dispatch] - ) -} - -function useNavigateToTokenDetails(): (currencyId: string) => void { - const navigation = useAppStackNavigation() - - return useCallback( - (currencyId: string): void => { - navigation.navigate(Screens.TokenDetails, { currencyId }) - }, - [navigation] - ) -} diff --git a/apps/mobile/src/app/hooks.test.ts b/apps/mobile/src/app/hooks.test.ts index c6eaa5616de..51553933924 100644 --- a/apps/mobile/src/app/hooks.test.ts +++ b/apps/mobile/src/app/hooks.test.ts @@ -1,7 +1,7 @@ import { renderHook } from '@testing-library/react-hooks' import { LayoutChangeEvent } from 'react-native' import { act } from 'react-test-renderer' -import { useShouldShowNativeKeyboard } from './hooks' +import { useDynamicFontSizing, useShouldShowNativeKeyboard } from './hooks' describe(useShouldShowNativeKeyboard, () => { it('returns false if layout calculation is pending', () => { @@ -50,3 +50,58 @@ describe(useShouldShowNativeKeyboard, () => { expect(result.current.isLayoutPending).toBe(false) }) }) + +const MAX_INPUT_FONT_SIZE = 42 +const MIN_INPUT_FONT_SIZE = 28 +const MAX_CHAR_PIXEL_WIDTH = 23 + +describe(useDynamicFontSizing, () => { + it('returns maxFontSize if text input element width is not set', () => { + const { result } = renderHook(() => + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + ) + + expect(result.current.fontSize).toBe(MAX_INPUT_FONT_SIZE) + }) + + it('returns maxFontSize as fontSize if text fits in the container', async () => { + const { result } = renderHook(() => + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + ) + + await act(() => { + result.current.onLayout({ nativeEvent: { layout: { width: 100 } } } as LayoutChangeEvent) + result.current.onSetFontSize('aaaa') + }) + + // 100 / 23 = 4.34 - 4 letters should fit in the container + expect(result.current.fontSize).toBe(MAX_INPUT_FONT_SIZE) + }) + + it('scales down font when text does not fit in the container', async () => { + const { result } = renderHook(() => + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + ) + + await act(() => { + result.current.onLayout({ nativeEvent: { layout: { width: 100 } } } as LayoutChangeEvent) + result.current.onSetFontSize('aaaaa') + }) + + // 100 / 23 = 4.34 - 5 letters should not fit in the container + expect(result.current.fontSize).toBeLessThan(MAX_INPUT_FONT_SIZE) + }) + + it("doesn't return font size less than minFontSize", async () => { + const { result } = renderHook(() => + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + ) + + await act(() => { + result.current.onLayout({ nativeEvent: { layout: { width: 100 } } } as LayoutChangeEvent) + result.current.onSetFontSize('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') + }) + + expect(result.current.fontSize).toBe(MIN_INPUT_FONT_SIZE) + }) +}) diff --git a/apps/mobile/src/app/hooks.ts b/apps/mobile/src/app/hooks.ts index b7b14968c6c..5106a0ac454 100644 --- a/apps/mobile/src/app/hooks.ts +++ b/apps/mobile/src/app/hooks.ts @@ -1,5 +1,5 @@ import { ThunkDispatch } from '@reduxjs/toolkit' -import { useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { LayoutChangeEvent } from 'react-native' import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import type { AppDispatch } from 'src/app/store' @@ -59,3 +59,48 @@ export function useShouldShowNativeKeyboard(): { isLayoutPending || showNativeKeyboard ? undefined : decimalPadY - MIN_INPUT_DECIMAL_PAD_GAP, } } + +export function useDynamicFontSizing( + maxCharWidthAtMaxFontSize: number, + maxFontSize: number, + minFontSize: number +): { + onLayout: (event: LayoutChangeEvent) => void + fontSize: number + onSetFontSize: (amount: string) => void +} { + const [fontSize, setFontSize] = useState(maxFontSize) + const textInputElementWidthRef = useRef(0) + + const onLayout = useCallback((event: LayoutChangeEvent) => { + if (textInputElementWidthRef.current) { + return + } + + const width = event.nativeEvent.layout.width + textInputElementWidthRef.current = width + }, []) + + const onSetFontSize = useCallback( + (amount: string) => { + const stringWidth = getStringWidth(amount, maxCharWidthAtMaxFontSize, fontSize, maxFontSize) + const scaledSize = fontSize * (textInputElementWidthRef.current / stringWidth) + const scaledSizeWithMin = Math.max(scaledSize, minFontSize) + const newFontSize = Math.round(Math.min(maxFontSize, scaledSizeWithMin)) + setFontSize(newFontSize) + }, + [fontSize, maxFontSize, minFontSize, maxCharWidthAtMaxFontSize] + ) + + return { onLayout, fontSize, onSetFontSize } +} + +const getStringWidth = ( + value: string, + maxCharWidthAtMaxFontSize: number, + currentFontSize: number, + maxFontSize: number +): number => { + const widthAtMaxFontSize = value.length * maxCharWidthAtMaxFontSize + return widthAtMaxFontSize * (currentFontSize / maxFontSize) +} diff --git a/apps/mobile/src/app/migrations.test.ts b/apps/mobile/src/app/migrations.test.ts index 4e5500d58cb..e9528c35a80 100644 --- a/apps/mobile/src/app/migrations.test.ts +++ b/apps/mobile/src/app/migrations.test.ts @@ -64,21 +64,22 @@ import { } from 'src/app/schema' import { persistConfig } from 'src/app/store' import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { initialBehaviorHistoryState } from 'src/features/behaviorHistory/slice' import { initialBiometricsSettingsState } from 'src/features/biometrics/slice' import { initialCloudBackupState } from 'src/features/CloudBackup/cloudBackupSlice' import { initialPasswordLockoutState } from 'src/features/CloudBackup/passwordLockoutSlice' +import { initialSearchHistoryState } from 'src/features/explore/searchHistorySlice' import { initialModalState } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { initialTelemetryState } from 'src/features/telemetry/slice' +import { initialTokensState } from 'src/features/tokens/tokensSlice' import { initialTweaksState } from 'src/features/tweaks/slice' import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice' import { ChainId } from 'wallet/src/constants/chains' -import { initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice' import { initialFavoritesState } from 'wallet/src/features/favorites/slice' import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' import { initialLanguageState } from 'wallet/src/features/language/slice' import { initialNotificationsState } from 'wallet/src/features/notifications/slice' -import { initialSearchHistoryState } from 'wallet/src/features/search/searchHistorySlice' -import { initialTokensState } from 'wallet/src/features/tokens/tokensSlice' import { initialTransactionsState, TransactionStateMap, @@ -94,7 +95,6 @@ import { SignerMnemonicAccount, } from 'wallet/src/features/wallet/accounts/types' import { initialWalletState, SwapProtectionSetting } from 'wallet/src/features/wallet/slice' -import { ModalName } from 'wallet/src/telemetry/constants' import { account, fiatOnRampTxDetailsFailed, txDetailsConfirmed } from 'wallet/src/test/fixtures' // helps with object assignment diff --git a/apps/mobile/src/app/migrations.ts b/apps/mobile/src/app/migrations.ts index b6daca9dbf8..fe3b81f4842 100644 --- a/apps/mobile/src/app/migrations.ts +++ b/apps/mobile/src/app/migrations.ts @@ -4,6 +4,7 @@ /* eslint-disable max-lines */ import dayjs from 'dayjs' +import { ModalName } from 'src/features/telemetry/constants' import { ChainId } from 'wallet/src/constants/chains' import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { AccountToNftData } from 'wallet/src/features/favorites/slice' @@ -18,7 +19,6 @@ import { } from 'wallet/src/features/transactions/types' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' -import { ModalName } from 'wallet/src/telemetry/constants' export const OLD_DEMO_ACCOUNT_ADDRESS = '0xdd0E380579dF30E38524F9477808d9eE37E2dEa6' diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx index 163656e96a9..a04e9f1f8b6 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx @@ -4,8 +4,8 @@ import { PreloadedState } from 'redux' import { AccountSwitcher } from 'src/app/modals/AccountSwitcherModal' import { MobileState } from 'src/app/reducer' import { initialModalState } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { render } from 'src/test/test-utils' -import { ModalName } from 'wallet/src/telemetry/constants' import { mockWalletPreloadedState } from 'wallet/src/test/fixtures' import { noOpFunction } from 'wallet/src/test/utils' diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx index 2d36d7ce790..67bba85f982 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -1,14 +1,19 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' import { Action } from 'redux' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' import { AccountList } from 'src/components/accounts/AccountList' +import { AddressDisplay } from 'src/components/AddressDisplay' +import { ActionSheetModal, MenuItemProp } from 'src/components/modals/ActionSheetModal' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' import { closeModal, openModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' -import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' +import { ElementName, ModalName } from 'src/features/telemetry/constants' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { openSettings } from 'src/utils/linking' import { Button, Flex, @@ -20,12 +25,7 @@ import { useSporeColors, } from 'ui/src' import { spacing } from 'ui/src/theme' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { ActionSheetModal, MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' -import { useUnitagUpdater } from 'wallet/src/features/unitags/context' -import { useCanAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { createAccountActions } from 'wallet/src/features/wallet/create/createAccountSaga' import { @@ -35,8 +35,6 @@ import { import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { selectAllAccountsSorted } from 'wallet/src/features/wallet/selectors' import { setAccountAsActive } from 'wallet/src/features/wallet/slice' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' -import { openSettings } from 'wallet/src/utils/linking' import { isAndroid } from 'wallet/src/utils/platform' export function AccountSwitcherModal(): JSX.Element { @@ -71,18 +69,11 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme const dispatch = useAppDispatch() const hasImportedSeedPhrase = useNativeAccountExists() const modalState = useAppSelector(selectModalState(ModalName.AccountSwitcher)) - const { refetchUnitagsCounter } = useUnitagUpdater() - const { canClaimUnitag, refetch: refetchCanAddressClaimUnitag } = useCanAddressClaimUnitag() const [showAddWalletModal, setShowAddWalletModal] = useState(false) const accounts = useAppSelector(selectAllAccountsSorted) - // Force refetch of canClaimUnitag if refetchUnitagsCounter changes - useEffect(() => { - refetchCanAddressClaimUnitag?.() - }, [refetchUnitagsCounter, refetchCanAddressClaimUnitag]) - const onPressAccount = useCallback( (address: Address) => { onClose() @@ -121,13 +112,12 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme dispatch(createAccountActions.trigger()) navigate(Screens.OnboardingStack, { - screen: canClaimUnitag ? UnitagScreens.ClaimUnitag : OnboardingScreens.EditName, + screen: OnboardingScreens.EditName, params: { - entryPoint: OnboardingEntryPoint.Sidebar, importType: hasImportedSeedPhrase ? ImportType.CreateAdditional : ImportType.CreateNew, + entryPoint: OnboardingEntryPoint.Sidebar, }, }) - setShowAddWalletModal(false) onClose() } @@ -187,7 +177,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme onClose() } - const options: MenuItemProp[] = [ + const options = [ { key: ElementName.CreateAccount, onPress: onPressCreateNewWallet, @@ -236,7 +226,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme } return options - }, [activeAccountAddress, canClaimUnitag, dispatch, hasImportedSeedPhrase, onClose, t]) + }, [activeAccountAddress, dispatch, hasImportedSeedPhrase, onClose, t]) const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress) diff --git a/apps/mobile/src/app/modals/AppModals.tsx b/apps/mobile/src/app/modals/AppModals.tsx index 59a0888f4a5..66a11eac62c 100644 --- a/apps/mobile/src/app/modals/AppModals.tsx +++ b/apps/mobile/src/app/modals/AppModals.tsx @@ -2,7 +2,6 @@ import React from 'react' import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal' import { ExperimentsModal } from 'src/app/modals/ExperimentsModal' import { ExploreModal } from 'src/app/modals/ExploreModal' -import { FiatOnRampAggregatorModal } from 'src/app/modals/FiatOnRampModalAggregator' import { SwapModal } from 'src/app/modals/SwapModal' import { TransferTokenModal } from 'src/app/modals/TransferTokenModal' import { LazyModalRenderer } from 'src/app/modals/utils' @@ -13,11 +12,11 @@ import { RestoreWalletModal } from 'src/components/RestoreWalletModal/RestoreWal import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal' import { WalletConnectModals } from 'src/components/WalletConnect/WalletConnectModals' import { LockScreenModal } from 'src/features/authentication/LockScreenModal' +import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal' import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal' -import { ScantasticModal } from 'src/features/scantastic/ScantasticModal' +import { ModalName } from 'src/features/telemetry/constants' import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal' import { SettingsLanguageModal } from 'src/screens/SettingsLanguageModal' -import { ModalName } from 'wallet/src/telemetry/constants' export function AppModals(): JSX.Element { return ( @@ -42,10 +41,6 @@ export function AppModals(): JSX.Element { - - - - diff --git a/apps/mobile/src/app/modals/ExperimentsModal.tsx b/apps/mobile/src/app/modals/ExperimentsModal.tsx index ca79561fd3d..93d32ae386a 100644 --- a/apps/mobile/src/app/modals/ExperimentsModal.tsx +++ b/apps/mobile/src/app/modals/ExperimentsModal.tsx @@ -3,7 +3,11 @@ import React, { useState } from 'react' import { ScrollView } from 'react-native-gesture-handler' import { Action } from 'redux' import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { Switch } from 'src/components/buttons/Switch' +import { TextInput } from 'src/components/input/TextInput' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { closeModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { selectCustomEndpoint } from 'src/features/tweaks/selectors' import { setCustomEndpoint } from 'src/features/tweaks/slice' import { @@ -22,16 +26,12 @@ import { useSporeColors, } from 'ui/src' import { spacing } from 'ui/src/theme' -import { Switch } from 'wallet/src/components/buttons/Switch' -import { TextInput } from 'wallet/src/components/input/TextInput' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { EXPERIMENT_NAMES, EXPERIMENT_VALUES_BY_EXPERIMENT, FEATURE_FLAGS, } from 'wallet/src/features/experiments/constants' import { useFeatureFlagWithExposureLoggingDisabled } from 'wallet/src/features/experiments/hooks' -import { ModalName } from 'wallet/src/telemetry/constants' export function ExperimentsModal(): JSX.Element { const insets = useDeviceInsets() @@ -193,7 +193,6 @@ function ExperimentRow({ name }: { name: string }): JSX.Element { const params = Object.entries(experiment.config.value).map(([key, value]) => ( { - dispatch(closeModal({ name: ModalName.FiatOnRampAggregator })) - }, [dispatch]) - - return ( - - - - ) -} diff --git a/apps/mobile/src/app/modals/SwapModal.tsx b/apps/mobile/src/app/modals/SwapModal.tsx index 54391180964..fa68a38234b 100644 --- a/apps/mobile/src/app/modals/SwapModal.tsx +++ b/apps/mobile/src/app/modals/SwapModal.tsx @@ -1,28 +1,19 @@ -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useEffect } from 'react' import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { BiometricsIcon } from 'src/components/icons/BiometricsIcon' -import { - useBiometricAppSettings, - useBiometricPrompt, - useOsBiometricAuthEnabled, -} from 'src/features/biometrics/hooks' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' +import { ModalName } from 'src/features/telemetry/constants' +import { updateSwapStartTimestamp } from 'src/features/telemetry/timing/slice' import { SwapFlow } from 'src/features/transactions/swap/SwapFlow' -import { getFocusOnCurrencyFieldFromInitialState } from 'src/features/transactions/swapRewrite/utils' -import { useWalletRestore } from 'src/features/wallet/hooks' +import { SwapFlow as SwapFlowRewrite } from 'src/features/transactions/swapRewrite/SwapFlow' import { useSporeColors } from 'ui/src' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { useSwapRewriteEnabled } from 'wallet/src/features/experiments/hooks' -import { SwapFormState } from 'wallet/src/features/transactions/contexts/SwapFormContext' -import { SwapFlow as SwapFlowRewrite } from 'wallet/src/features/transactions/swap/SwapFlow' -import { ModalName } from 'wallet/src/telemetry/constants' -import { updateSwapStartTimestamp } from 'wallet/src/telemetry/timing/slice' export function SwapModal(): JSX.Element { const colors = useSporeColors() const appDispatch = useAppDispatch() - const { initialState } = useAppSelector(selectModalState(ModalName.Swap)) + const modalState = useAppSelector(selectModalState(ModalName.Swap)) const shouldShowSwapRewrite = useSwapRewriteEnabled() @@ -35,40 +26,8 @@ export function SwapModal(): JSX.Element { appDispatch(updateSwapStartTimestamp({ timestamp: Date.now() })) }, [appDispatch]) - const { openWalletRestoreModal, walletNeedsRestore } = useWalletRestore() - - const swapRewritePrefilledState = useMemo( - (): SwapFormState | undefined => - initialState - ? { - customSlippageTolerance: initialState.customSlippageTolerance, - exactAmountFiat: initialState.exactAmountFiat, - exactAmountToken: initialState.exactAmountToken, - exactCurrencyField: initialState.exactCurrencyField, - focusOnCurrencyField: getFocusOnCurrencyFieldFromInitialState(initialState), - input: initialState.input ?? undefined, - output: initialState.output ?? undefined, - selectingCurrencyField: initialState.selectingCurrencyField, - txId: initialState.txId, - isFiatMode: false, - isSubmitting: false, - } - : undefined, - [initialState] - ) - - const { requiredForTransactions: requiresBiometrics } = useBiometricAppSettings() - const { trigger: biometricsTrigger } = useBiometricPrompt() - return shouldShowSwapRewrite ? ( - } - authTrigger={requiresBiometrics ? biometricsTrigger : undefined} - openWalletRestoreModal={openWalletRestoreModal} - prefilledState={swapRewritePrefilledState} - walletNeedsRestore={Boolean(walletNeedsRestore)} - onClose={onClose} - /> + ) : ( - + ) } - -function SwapBiometricsIcon(): JSX.Element | null { - const isBiometricAuthEnabled = useOsBiometricAuthEnabled() - const { requiredForTransactions } = useBiometricAppSettings() - - return isBiometricAuthEnabled && requiredForTransactions ? : null -} diff --git a/apps/mobile/src/app/modals/TransferTokenModal.tsx b/apps/mobile/src/app/modals/TransferTokenModal.tsx index c6b07f9a73e..1a42da7bd7d 100644 --- a/apps/mobile/src/app/modals/TransferTokenModal.tsx +++ b/apps/mobile/src/app/modals/TransferTokenModal.tsx @@ -1,14 +1,14 @@ import React, { useCallback } from 'react' import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' +import { ModalName } from 'src/features/telemetry/constants' import { TransferFlow as TransferFlowRewrite } from 'src/features/transactions/swapRewrite/transfer/TransferFlow' import { TransferFlow } from 'src/features/transactions/transfer/TransferFlow' import { useSporeColors } from 'ui/src' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' -import { ModalName } from 'wallet/src/telemetry/constants' export function TransferTokenModal(): JSX.Element { const colors = useSporeColors() diff --git a/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx index 5ca15a28a93..0824b03d119 100644 --- a/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx +++ b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx @@ -1,15 +1,16 @@ import { useTranslation } from 'react-i18next' import { navigate } from 'src/app/navigation/rootNavigation' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { closeModal, openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { OnboardingScreens, Screens } from 'src/screens/Screens' -import { Button, Flex, Text, useIsDarkMode } from 'ui/src' +import { Button, Flex, Text } from 'ui/src' import ViewOnlyWalletDark from 'ui/src/assets/graphics/view-only-wallet-dark.svg' import ViewOnlyWalletLight from 'ui/src/assets/graphics/view-only-wallet-light.svg' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' const WALLET_IMAGE_ASPECT_RATIO = 327 / 215 diff --git a/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap index e7d65e4630b..e600b1d6b62 100644 --- a/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap +++ b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap @@ -4,6 +4,7 @@ exports[`AccountSwitcher renders correctly 1`] = ` - - - Test Account - - + Test Account + () const AppStack = createNativeStackNavigator() const ExploreStack = createNativeStackNavigator() -const FiatOnRampStack = createNativeStackNavigator() const SettingsStack = createNativeStackNavigator() -const UnitagStack = createStackNavigator() +const UnitagStack = createNativeStackNavigator() function SettingsStackGroup(): JSX.Element { return ( @@ -177,41 +171,7 @@ export function ExploreStackNavigator(): JSX.Element { ) } -export function FiatOnRampStackNavigator(): JSX.Element { - return ( - - - - - - - - - - - ) -} - const renderEmptyBackImage = (): JSX.Element => <> -const renderHeaderBackImage = (): JSX.Element => ( - -) export function OnboardingStackNavigator(): JSX.Element { const colors = useSporeColors() @@ -221,6 +181,10 @@ export function OnboardingStackNavigator(): JSX.Element { ? SeedPhraseInputScreenV2 : SeedPhraseInputScreen + const renderHeaderBackImage = (): JSX.Element => ( + + ) + return ( - - - + ) diff --git a/apps/mobile/src/app/navigation/rootNavigation.ts b/apps/mobile/src/app/navigation/rootNavigation.ts index 2bcb36af098..a43a3d60a2f 100644 --- a/apps/mobile/src/app/navigation/rootNavigation.ts +++ b/apps/mobile/src/app/navigation/rootNavigation.ts @@ -10,7 +10,7 @@ export type RootNavigationArgs = function isNavigationRefReady(): boolean { if (!navigationRef.isReady()) { - logger.error(new Error('Navigator was called before it was initialized'), { + logger.error('Navigator was called before it was initialized', { tags: { file: 'rootNavigation', function: 'navigate' }, }) return false diff --git a/apps/mobile/src/app/navigation/types.ts b/apps/mobile/src/app/navigation/types.ts index fd5e203d235..dde08810850 100644 --- a/apps/mobile/src/app/navigation/types.ts +++ b/apps/mobile/src/app/navigation/types.ts @@ -7,7 +7,7 @@ import { import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack' import { EducationContentType } from 'src/components/education' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' -import { FiatOnRampScreens, OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' +import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' import { NFTItem } from 'wallet/src/features/nfts/types' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' @@ -36,12 +36,6 @@ export type ExploreStackParamList = { } } -export type FiatOnRampStackParamList = { - [FiatOnRampScreens.AmountInput]: undefined - [FiatOnRampScreens.ServiceProviders]: undefined - [FiatOnRampScreens.Connecting]: undefined -} - export type SettingsStackParamList = { [Screens.Dev]: undefined [Screens.Settings]: undefined @@ -66,18 +60,6 @@ export type OnboardingStackBaseParams = { entryPoint: OnboardingEntryPoint } -export type SharedUnitagScreenParams = { - [UnitagScreens.ClaimUnitag]: { - entryPoint: OnboardingScreens.Landing | OnboardingEntryPoint.Sidebar | Screens.Home - importType: ImportType.CreateNew | ImportType.CreateAdditional - } - [UnitagScreens.ChooseProfilePicture]: { - entryPoint: OnboardingScreens.Landing | OnboardingEntryPoint.Sidebar | Screens.Home - importType: ImportType.CreateNew | ImportType.CreateAdditional - unitag: string - } -} - export type OnboardingStackParamList = { [OnboardingScreens.BackupManual]: OnboardingStackBaseParams [OnboardingScreens.BackupCloudPasswordCreate]: { @@ -89,7 +71,7 @@ export type OnboardingStackParamList = { [OnboardingScreens.Landing]: OnboardingStackBaseParams [OnboardingScreens.EditName]: OnboardingStackBaseParams [OnboardingScreens.Notifications]: OnboardingStackBaseParams - [OnboardingScreens.WelcomeWallet]: OnboardingStackBaseParams + [OnboardingScreens.QRAnimation]: OnboardingStackBaseParams [OnboardingScreens.Security]: OnboardingStackBaseParams // import @@ -102,9 +84,14 @@ export type OnboardingStackParamList = { [OnboardingScreens.SeedPhraseInput]: OnboardingStackBaseParams [OnboardingScreens.SelectWallet]: OnboardingStackBaseParams [OnboardingScreens.WatchWallet]: OnboardingStackBaseParams -} & SharedUnitagScreenParams +} -export type UnitagStackParamList = SharedUnitagScreenParams & { +export type UnitagStackParamList = { + [UnitagScreens.ClaimUnitag]: { entryPoint: OnboardingScreens.Landing | Screens.Home } + [UnitagScreens.ChooseProfilePicture]: { + entryPoint: OnboardingScreens.Landing | Screens.Home + unitag: string + } [UnitagScreens.UnitagConfirmation]: { unitag: string address: Address @@ -112,8 +99,6 @@ export type UnitagStackParamList = SharedUnitagScreenParams & { } [UnitagScreens.EditProfile]: { address: Address - unitag: string - entryPoint: UnitagScreens.UnitagConfirmation | Screens.SettingsWallet } } @@ -168,8 +153,7 @@ export type RootParamList = AppStackParamList & ExploreStackParamList & OnboardingStackParamList & SettingsStackParamList & - UnitagStackParamList & - FiatOnRampStackParamList + UnitagStackParamList export const useAppStackNavigation = (): AppStackNavigationProp => useNavigation() diff --git a/apps/mobile/src/app/reducer.ts b/apps/mobile/src/app/reducer.ts index c5ab1edbe9e..21ba77b1be1 100644 --- a/apps/mobile/src/app/reducer.ts +++ b/apps/mobile/src/app/reducer.ts @@ -1,9 +1,13 @@ import { combineReducers } from '@reduxjs/toolkit' +import { behaviorHistoryReducer } from 'src/features/behaviorHistory/slice' +import { biometricSettingsReducer } from 'src/features/biometrics/slice' import { cloudBackupReducer } from 'src/features/CloudBackup/cloudBackupSlice' import { passwordLockoutReducer } from 'src/features/CloudBackup/passwordLockoutSlice' -import { biometricSettingsReducer } from 'src/features/biometrics/slice' +import { searchHistoryReducer } from 'src/features/explore/searchHistorySlice' import { modalsReducer } from 'src/features/modals/modalSlice' import { telemetryReducer } from 'src/features/telemetry/slice' +import { timingReducer } from 'src/features/telemetry/timing/slice' +import { tokensReducer } from 'src/features/tokens/tokensSlice' import { tweaksReducer } from 'src/features/tweaks/slice' import { walletConnectReducer } from 'src/features/walletConnect/walletConnectSlice' import { sharedReducers } from 'wallet/src/state/reducer' @@ -11,12 +15,16 @@ import { monitoredSagaReducers } from './saga' const reducers = { ...sharedReducers, + behaviorHistory: behaviorHistoryReducer, biometricSettings: biometricSettingsReducer, cloudBackup: cloudBackupReducer, modals: modalsReducer, passwordLockout: passwordLockoutReducer, saga: monitoredSagaReducers, + searchHistory: searchHistoryReducer, telemetry: telemetryReducer, + timing: timingReducer, + tokens: tokensReducer, tweaks: tweaksReducer, walletConnect: walletConnectReducer, } as const diff --git a/apps/mobile/src/app/saga.ts b/apps/mobile/src/app/saga.ts index 459c8e9cd05..4e5095a466e 100644 --- a/apps/mobile/src/app/saga.ts +++ b/apps/mobile/src/app/saga.ts @@ -4,24 +4,25 @@ import { cloudBackupsManagerSaga } from 'src/features/CloudBackup/saga' import { deepLinkWatcher } from 'src/features/deepLinking/handleDeepLinkSaga' import { firebaseDataWatcher } from 'src/features/firebase/firebaseDataSaga' import { modalWatcher } from 'src/features/modals/saga' +import { notificationWatcher } from 'src/features/notifications/notificationWatcherSaga' import { telemetrySaga } from 'src/features/telemetry/saga' -import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga' -import { walletConnectSaga } from 'src/features/walletConnect/saga' -import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga' -import { spawn } from 'typed-redux-saga' -import { appLanguageWatcherSaga } from 'wallet/src/features/language/saga' import { swapActions, swapReducer, swapSaga, swapSagaName, -} from 'wallet/src/features/transactions/swap/swapSaga' +} from 'src/features/transactions/swap/swapSaga' import { tokenWrapActions, tokenWrapReducer, tokenWrapSaga, tokenWrapSagaName, -} from 'wallet/src/features/transactions/swap/wrapSaga' +} from 'src/features/transactions/swap/wrapSaga' +import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga' +import { walletConnectSaga } from 'src/features/walletConnect/saga' +import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga' +import { spawn } from 'typed-redux-saga' +import { appLanguageWatcherSaga } from 'wallet/src/features/language/saga' import { transactionWatcher } from 'wallet/src/features/transactions/transactionWatcherSaga' import { editAccountActions, @@ -52,6 +53,7 @@ const sagas = [ deepLinkWatcher, firebaseDataWatcher, modalWatcher, + notificationWatcher, pendingAccountSaga, restoreMnemonicCompleteWatcher, signWcRequestSaga, diff --git a/apps/mobile/src/app/schema.ts b/apps/mobile/src/app/schema.ts index fb12b080295..2b801044a4f 100644 --- a/apps/mobile/src/app/schema.ts +++ b/apps/mobile/src/app/schema.ts @@ -1,7 +1,7 @@ +import { ModalName } from 'src/features/telemetry/constants' import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' import { initialLanguageState } from 'wallet/src/features/language/slice' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' -import { ModalName } from 'wallet/src/telemetry/constants' // only add fields that are persisted export const initialSchema = { diff --git a/apps/mobile/src/app/store.ts b/apps/mobile/src/app/store.ts index babe004c92e..13fe71dd7f3 100644 --- a/apps/mobile/src/app/store.ts +++ b/apps/mobile/src/app/store.ts @@ -2,7 +2,7 @@ import type { Middleware, PayloadAction, PreloadedState } from '@reduxjs/toolkit import { isRejectedWithValue } from '@reduxjs/toolkit' import * as Sentry from '@sentry/react' import { MMKV } from 'react-native-mmkv' -import { Storage, persistReducer, persistStore } from 'redux-persist' +import { persistReducer, persistStore, Storage } from 'redux-persist' import createMigrate from 'src/app/createMigrate' import { migrations } from 'src/app/migrations' import { isNonJestDev } from 'utilities/src/environment' @@ -11,7 +11,7 @@ import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiat import { importAccountSagaName } from 'wallet/src/features/wallet/import/importAccountSaga' import { createStore } from 'wallet/src/state' import { RootReducerNames } from 'wallet/src/state/reducer' -import { MobileState, ReducerNames, mobileReducer } from './reducer' +import { mobileReducer, MobileState, ReducerNames } from './reducer' import { mobileSaga } from './saga' const storage = new MMKV() @@ -107,9 +107,9 @@ if (isNonJestDev) { middlewares.push(createDebugger()) } +// eslint-disable-next-line prettier/prettier, @typescript-eslint/explicit-function-return-type export const setupStore = ( preloadedState?: PreloadedState - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type ) => { return createStore({ reducer: persistedReducer, diff --git a/packages/wallet/src/components/accounts/AccountIcon.tsx b/apps/mobile/src/components/AccountIcon.tsx similarity index 91% rename from packages/wallet/src/components/accounts/AccountIcon.tsx rename to apps/mobile/src/components/AccountIcon.tsx index 3d27c4acd06..95af95be01b 100644 --- a/packages/wallet/src/components/accounts/AccountIcon.tsx +++ b/apps/mobile/src/components/AccountIcon.tsx @@ -1,6 +1,9 @@ +import React from 'react' import { StyleSheet } from 'react-native' import Svg, { Defs, RadialGradient as RadialGradientSVG, Rect, Stop } from 'react-native-svg' -import { Flex, Icons, Unicon, useUniconColors } from 'ui/src' +import { Unicon } from 'src/components/unicons/Unicon' +import { useUniconColors } from 'src/components/unicons/utils' +import { Flex, Icons } from 'ui/src' import { spacing } from 'ui/src/theme' import { RemoteImage } from 'wallet/src/features/images/RemoteImage' @@ -13,7 +16,6 @@ export interface AccountIconProps { address: string avatarUri?: string | null showBackground?: boolean // Display images with solid background. - showBorder?: boolean // Display border stroke around image backgroundPadding?: number } @@ -23,7 +25,6 @@ export function AccountIcon({ address, avatarUri, showBackground, - showBorder, backgroundPadding = spacing.spacing12, }: AccountIconProps): JSX.Element { // add padding to unicon if background is displayed @@ -48,16 +49,13 @@ export function AccountIcon({ return ( {avatarUri ? ( diff --git a/packages/wallet/src/components/accounts/AddressDisplay.tsx b/apps/mobile/src/components/AddressDisplay.tsx similarity index 57% rename from packages/wallet/src/components/accounts/AddressDisplay.tsx rename to apps/mobile/src/components/AddressDisplay.tsx index dad3132bb78..6895985963c 100644 --- a/packages/wallet/src/components/accounts/AddressDisplay.tsx +++ b/apps/mobile/src/components/AddressDisplay.tsx @@ -1,18 +1,18 @@ import { impactAsync } from 'expo-haptics' -import { PropsWithChildren, useMemo } from 'react' +import { default as React, PropsWithChildren, useMemo } from 'react' import { FlexAlignType } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { AccountIcon, AccountIconProps } from 'src/components/AccountIcon' +import { NotificationBadge } from 'src/components/notifications/Badge' +import { ElementName } from 'src/features/telemetry/constants' +import { setClipboard } from 'src/utils/clipboard' import { ColorTokens, Flex, Icons, SpaceTokens, Text, TextProps, TouchableArea } from 'ui/src' import { fonts } from 'ui/src/theme' -import { AccountIcon, AccountIconProps } from 'wallet/src/components/accounts/AccountIcon' -import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' +import { useENSAvatar } from 'wallet/src/features/ens/api' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' -import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' -import { DisplayNameType } from 'wallet/src/features/wallet/types' -import { useAppDispatch } from 'wallet/src/state' -import { ElementName } from 'wallet/src/telemetry/constants' +import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' -import { setClipboard } from 'wallet/src/utils/clipboard' type AddressDisplayProps = { address: string @@ -29,17 +29,9 @@ type AddressDisplayProps = { showAccountIcon?: boolean contentAlign?: FlexAlignType showIconBackground?: boolean - showIconBorder?: boolean - includeUnitagSuffix?: boolean textAlign?: FlexAlignType horizontalGap?: SpaceTokens - notificationsBadgeContainer?: ({ - children, - address, - }: { - children: JSX.Element - address: string - }) => JSX.Element + showNotificationsBadge?: boolean gapBetweenLines?: SpaceTokens } & Pick @@ -73,7 +65,6 @@ function getLineHeightForAdjustedFontSize(nameLength: number): number { } /** Helper component to display identicon and formatted address */ - export function AddressDisplay({ allowFontScaling = true, address, @@ -90,19 +81,16 @@ export function AddressDisplay({ textAlign, contentAlign = 'center', // vertical alignment of all items showIconBackground, - showIconBorder, horizontalGap = '$spacing12', showViewOnlyBadge = false, - notificationsBadgeContainer, - includeUnitagSuffix = false, + showNotificationsBadge = false, gapBetweenLines = '$none', }: AddressDisplayProps): JSX.Element { const dispatch = useAppDispatch() - const displayName = useDisplayName(address, { includeUnitagSuffix }) - const { avatar } = useAvatar(address) + const displayName = useDisplayName(address) + const { data: avatar } = useENSAvatar(address) - const showAddressAsSubtitle = - !hideAddressInSubtitle && displayName?.type !== DisplayNameType.Address + const showAddressAsSubtitle = !hideAddressInSubtitle && displayName?.type !== 'address' const onPressCopyAddress = async (): Promise => { if (!address) { @@ -130,14 +118,13 @@ export function AddressDisplay({ address={address} avatarUri={avatar} showBackground={showIconBackground} - showBorder={showIconBorder} showViewOnlyBadge={showViewOnlyBadge} size={size} /> ) - }, [address, avatar, showIconBackground, showIconBorder, showViewOnlyBadge, size]) + }, [address, avatar, showIconBackground, showViewOnlyBadge, size]) - const name = displayName?.name ?? '' + const name = displayName?.name || '' // since adjustsFontSizeToFit doesnt really work adjusting line height properly // manually adjust lineHeight things to keep vertical center @@ -154,75 +141,51 @@ export function AddressDisplay({ return ( {showAccountIcon && - (notificationsBadgeContainer - ? notificationsBadgeContainer({ children: icon, address }) - : icon)} + (showNotificationsBadge ? ( + {icon} + ) : ( + icon + ))} - + + {name} + {showCopy && !showAddressAsSubtitle && ( )} {showAddressAsSubtitle && ( - + + + + {sanitizeAddressText(shortenAddress(address))} + + {showCopy && } + + )} ) } - -const AddressSubtitle = ({ - address, - captionTextColor, - captionVariant, - captionSize, - showCopy, - showCopyWrapperButton, - onPressCopyAddress, -}: { captionSize: number; onPressCopyAddress: () => Promise } & Pick< - AddressDisplayProps, - 'address' | 'captionTextColor' | 'captionVariant' | 'showCopy' | 'showCopyWrapperButton' ->): JSX.Element => ( - - - - {sanitizeAddressText(shortenAddress(address))} - - {showCopy && } - - -) diff --git a/packages/wallet/src/components/NFT/NFTTransfer.tsx b/apps/mobile/src/components/NFT/NFTTransfer.tsx similarity index 97% rename from packages/wallet/src/components/NFT/NFTTransfer.tsx rename to apps/mobile/src/components/NFT/NFTTransfer.tsx index 9af8464a4ea..b913e6b4ea9 100644 --- a/packages/wallet/src/components/NFT/NFTTransfer.tsx +++ b/apps/mobile/src/components/NFT/NFTTransfer.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { Flex, Text } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' diff --git a/packages/wallet/src/components/network/NetworkFee.test.tsx b/apps/mobile/src/components/Network/NetworkFee.test.tsx similarity index 89% rename from packages/wallet/src/components/network/NetworkFee.test.tsx rename to apps/mobile/src/components/Network/NetworkFee.test.tsx index ebc40b8ab51..6260d39183e 100644 --- a/packages/wallet/src/components/network/NetworkFee.test.tsx +++ b/apps/mobile/src/components/Network/NetworkFee.test.tsx @@ -1,7 +1,8 @@ +import React from 'react' +import { NetworkFee } from 'src/components/Network/NetworkFee' +import { render } from 'src/test/test-utils' import { ChainId } from 'wallet/src/constants/chains' -import { render } from 'wallet/src/test/test-utils' import { noOpFunction } from 'wallet/src/test/utils' -import { NetworkFee } from './NetworkFee' jest.mock('wallet/src/features/gas/hooks', () => { return { diff --git a/packages/wallet/src/components/network/NetworkFee.tsx b/apps/mobile/src/components/Network/NetworkFee.tsx similarity index 91% rename from packages/wallet/src/components/network/NetworkFee.tsx rename to apps/mobile/src/components/Network/NetworkFee.tsx index 09f0e1866c3..7a8270d405c 100644 --- a/packages/wallet/src/components/network/NetworkFee.tsx +++ b/apps/mobile/src/components/Network/NetworkFee.tsx @@ -1,9 +1,10 @@ +import React from 'react' import { useTranslation } from 'react-i18next' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { InlineNetworkPill } from 'src/components/Network/NetworkPill' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill' import { ChainId } from 'wallet/src/constants/chains' import { useUSDValue } from 'wallet/src/features/gas/hooks' import { GasFeeResult } from 'wallet/src/features/gas/types' diff --git a/packages/wallet/src/components/network/NetworkOption.tsx b/apps/mobile/src/components/Network/NetworkOption.tsx similarity index 92% rename from packages/wallet/src/components/network/NetworkOption.tsx rename to apps/mobile/src/components/Network/NetworkOption.tsx index 6cdcc114242..41beec8e564 100644 --- a/packages/wallet/src/components/network/NetworkOption.tsx +++ b/apps/mobile/src/components/Network/NetworkOption.tsx @@ -1,9 +1,10 @@ +import { default as React } from 'react' import { useTranslation } from 'react-i18next' import { Flex, Separator, Text, useSporeColors } from 'ui/src' import Check from 'ui/src/assets/icons/check.svg' import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' -import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' const NETWORK_OPTION_ICON_SIZE = iconSizes.icon24 diff --git a/packages/wallet/src/components/network/NetworkPill.test.tsx b/apps/mobile/src/components/Network/NetworkPill.test.tsx similarity index 78% rename from packages/wallet/src/components/network/NetworkPill.test.tsx rename to apps/mobile/src/components/Network/NetworkPill.test.tsx index f010d8d4ab6..091dc1b9a93 100644 --- a/packages/wallet/src/components/network/NetworkPill.test.tsx +++ b/apps/mobile/src/components/Network/NetworkPill.test.tsx @@ -1,6 +1,7 @@ +import React from 'react' +import { InlineNetworkPill, NetworkPill } from 'src/components/Network/NetworkPill' +import { render } from 'src/test/test-utils' import { ChainId } from 'wallet/src/constants/chains' -import { render } from 'wallet/src/test/test-utils' -import { InlineNetworkPill, NetworkPill } from './NetworkPill' describe(NetworkPill, () => { it('renders a NetworkPill without image', () => { diff --git a/packages/wallet/src/components/network/NetworkPill.tsx b/apps/mobile/src/components/Network/NetworkPill.tsx similarity index 88% rename from packages/wallet/src/components/network/NetworkPill.tsx rename to apps/mobile/src/components/Network/NetworkPill.tsx index 201a8fcc195..85a39beafc1 100644 --- a/packages/wallet/src/components/network/NetworkPill.tsx +++ b/apps/mobile/src/components/Network/NetworkPill.tsx @@ -1,8 +1,8 @@ -import { ComponentProps } from 'react' +import React, { ComponentProps } from 'react' +import { Pill } from 'src/components/text/Pill' import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' -import { Pill } from 'wallet/src/components/text/Pill' -import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' import { useNetworkColors } from 'wallet/src/utils/colors' export type NetworkPillProps = { diff --git a/packages/wallet/src/components/network/__snapshots__/NetworkFee.test.tsx.snap b/apps/mobile/src/components/Network/__snapshots__/NetworkFee.test.tsx.snap similarity index 96% rename from packages/wallet/src/components/network/__snapshots__/NetworkFee.test.tsx.snap rename to apps/mobile/src/components/Network/__snapshots__/NetworkFee.test.tsx.snap index cde3efa6480..93359586628 100644 --- a/packages/wallet/src/components/network/__snapshots__/NetworkFee.test.tsx.snap +++ b/apps/mobile/src/components/Network/__snapshots__/NetworkFee.test.tsx.snap @@ -155,13 +155,17 @@ exports[`NetworkFee renders a NetworkFee in a loading state 1`] = ` { "alignItems": "center", "backgroundColor": "#3939391a", + "borderBottomColor": "transparent", "borderBottomLeftRadius": 8, "borderBottomRightRadius": 8, "borderBottomWidth": 1, "borderColor": "transparent", + "borderLeftColor": "transparent", "borderLeftWidth": 1, + "borderRightColor": "transparent", "borderRightWidth": 1, "borderStyle": "solid", + "borderTopColor": "transparent", "borderTopLeftRadius": 8, "borderTopRightRadius": 8, "borderTopWidth": 1, @@ -209,6 +213,7 @@ exports[`NetworkFee renders a NetworkFee in a loading state 1`] = ` sentry-label="SpinningLoader" style={ { + "alignItems": "stretch", "flexDirection": "column", "transform": [ { @@ -445,13 +450,17 @@ exports[`NetworkFee renders a NetworkFee in an error state 1`] = ` { "alignItems": "center", "backgroundColor": "#3939391a", + "borderBottomColor": "transparent", "borderBottomLeftRadius": 8, "borderBottomRightRadius": 8, "borderBottomWidth": 1, "borderColor": "transparent", + "borderLeftColor": "transparent", "borderLeftWidth": 1, + "borderRightColor": "transparent", "borderRightWidth": 1, "borderStyle": "solid", + "borderTopColor": "transparent", "borderTopLeftRadius": 8, "borderTopRightRadius": 8, "borderTopWidth": 1, @@ -657,13 +666,17 @@ exports[`NetworkFee renders a NetworkFee normally 1`] = ` { "alignItems": "center", "backgroundColor": "#3939391a", + "borderBottomColor": "transparent", "borderBottomLeftRadius": 8, "borderBottomRightRadius": 8, "borderBottomWidth": 1, "borderColor": "transparent", + "borderLeftColor": "transparent", "borderLeftWidth": 1, + "borderRightColor": "transparent", "borderRightWidth": 1, "borderStyle": "solid", + "borderTopColor": "transparent", "borderTopLeftRadius": 8, "borderTopRightRadius": 8, "borderTopWidth": 1, diff --git a/packages/wallet/src/components/network/__snapshots__/NetworkPill.test.tsx.snap b/apps/mobile/src/components/Network/__snapshots__/NetworkPill.test.tsx.snap similarity index 86% rename from packages/wallet/src/components/network/__snapshots__/NetworkPill.test.tsx.snap rename to apps/mobile/src/components/Network/__snapshots__/NetworkPill.test.tsx.snap index ffb53d93538..dee2512bbe6 100644 --- a/packages/wallet/src/components/network/__snapshots__/NetworkPill.test.tsx.snap +++ b/apps/mobile/src/components/Network/__snapshots__/NetworkPill.test.tsx.snap @@ -6,13 +6,17 @@ exports[`NetworkPill renders a NetworkPill with border 1`] = ` { "alignItems": "center", "backgroundColor": "#2098531a", + "borderBottomColor": "transparent", "borderBottomLeftRadius": 999999, "borderBottomRightRadius": 999999, "borderBottomWidth": 1, "borderColor": "#209853", + "borderLeftColor": "transparent", "borderLeftWidth": 1, + "borderRightColor": "transparent", "borderRightWidth": 1, "borderStyle": "solid", + "borderTopColor": "transparent", "borderTopLeftRadius": 999999, "borderTopRightRadius": 999999, "borderTopWidth": 1, @@ -51,13 +55,17 @@ exports[`NetworkPill renders a NetworkPill without image 1`] = ` { "alignItems": "center", "backgroundColor": "#2098531a", + "borderBottomColor": "transparent", "borderBottomLeftRadius": 999999, "borderBottomRightRadius": 999999, "borderBottomWidth": 1, "borderColor": "transparent", + "borderLeftColor": "transparent", "borderLeftWidth": 1, + "borderRightColor": "transparent", "borderRightWidth": 1, "borderStyle": "solid", + "borderTopColor": "transparent", "borderTopLeftRadius": 999999, "borderTopRightRadius": 999999, "borderTopWidth": 1, @@ -96,13 +104,17 @@ exports[`NetworkPill renders an InlineNetworkPill 1`] = ` { "alignItems": "center", "backgroundColor": "#2098531a", + "borderBottomColor": "transparent", "borderBottomLeftRadius": 8, "borderBottomRightRadius": 8, "borderBottomWidth": 1, "borderColor": "transparent", + "borderLeftColor": "transparent", "borderLeftWidth": 1, + "borderRightColor": "transparent", "borderRightWidth": 1, "borderStyle": "solid", + "borderTopColor": "transparent", "borderTopLeftRadius": 8, "borderTopRightRadius": 8, "borderTopWidth": 1, diff --git a/packages/wallet/src/components/network/hooks.tsx b/apps/mobile/src/components/Network/hooks.tsx similarity index 81% rename from packages/wallet/src/components/network/hooks.tsx rename to apps/mobile/src/components/Network/hooks.tsx index 2e5a5340f0b..8891095f754 100644 --- a/packages/wallet/src/components/network/hooks.tsx +++ b/apps/mobile/src/components/Network/hooks.tsx @@ -1,7 +1,7 @@ -import { useMemo } from 'react' -import { NetworkOption } from 'wallet/src/components/network/NetworkOption' +import { default as React, useMemo } from 'react' +import { NetworkOption } from 'src/components/Network/NetworkOption' +import { ElementName } from 'src/features/telemetry/constants' import { ALL_SUPPORTED_CHAIN_IDS, ChainId } from 'wallet/src/constants/chains' -import { ElementName } from 'wallet/src/telemetry/constants' export function useNetworkOptions({ onPress, diff --git a/apps/mobile/src/components/PriceExplorer/AnimatedDecimalNumber.tsx b/apps/mobile/src/components/PriceExplorer/AnimatedDecimalNumber.tsx index 3e64d28ec58..dbe154d9f1d 100644 --- a/apps/mobile/src/components/PriceExplorer/AnimatedDecimalNumber.tsx +++ b/apps/mobile/src/components/PriceExplorer/AnimatedDecimalNumber.tsx @@ -3,7 +3,7 @@ import { useWindowDimensions } from 'react-native' import { useAnimatedStyle, useDerivedValue } from 'react-native-reanimated' import { AnimatedText } from 'src/components/text/AnimatedText' import { Flex, useDeviceDimensions, useSporeColors } from 'ui/src' -import { TextVariantTokens, fonts } from 'ui/src/theme' +import { fonts, TextVariantTokens } from 'ui/src/theme' import { ValueAndFormatted } from './usePrice' type AnimatedDecimalNumberProps = { diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx index 1500a4e1c1d..d076a09eacf 100644 --- a/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx @@ -1,16 +1,16 @@ import { ImpactFeedbackStyle } from 'expo-haptics' import { memo, useMemo } from 'react' import { I18nManager } from 'react-native' -import { SharedValue, useDerivedValue } from 'react-native-reanimated' +import { SharedValue } from 'react-native-reanimated' import { LineChart, LineChartProvider, TLineChartDataProp } from 'react-native-wagmi-charts' +import { Loader } from 'src/components/loading' +import { CURSOR_INNER_SIZE, CURSOR_SIZE } from 'src/components/PriceExplorer/constants' import PriceExplorerAnimatedNumber from 'src/components/PriceExplorer/PriceExplorerAnimatedNumber' import { PriceExplorerError } from 'src/components/PriceExplorer/PriceExplorerError' import { DatetimeText, RelativeChangeText } from 'src/components/PriceExplorer/Text' import { TimeRangeGroup } from 'src/components/PriceExplorer/TimeRangeGroup' -import { CURSOR_INNER_SIZE, CURSOR_SIZE } from 'src/components/PriceExplorer/constants' import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' import { useLineChartPrice } from 'src/components/PriceExplorer/usePrice' -import { Loader } from 'src/components/loading' import { invokeImpact } from 'src/utils/haptic' import { Flex } from 'ui/src' import { spacing } from 'ui/src/theme' @@ -24,7 +24,7 @@ type PriceTextProps = { loading: boolean relativeChange?: SharedValue numberOfDigits: PriceNumberOfDigits - spotPrice?: SharedValue + spotPrice?: number } function PriceTextSection({ loading, numberOfDigits, spotPrice }: PriceTextProps): JSX.Element { @@ -84,15 +84,14 @@ export const PriceExplorer = memo(function PriceExplorer({ return { lastPricePoint: lastPoint, convertedPriceHistory: priceHistory } }, [data, conversionRate]) - const convertedSpotValue = useDerivedValue(() => conversionRate * (data?.spot?.value?.value ?? 0)) const convertedSpot = useMemo((): TokenSpotData | undefined => { return ( data?.spot && { ...data?.spot, - value: convertedSpotValue, + value: { value: conversionRate * (data?.spot?.value?.value ?? 0) }, } ) - }, [data, convertedSpotValue]) + }, [data, conversionRate]) if ( !loading && @@ -189,7 +188,7 @@ function PriceExplorerChart({ loading={loading} numberOfDigits={numberOfDigits} relativeChange={spot?.relativeChange} - spotPrice={spot?.value} + spotPrice={spot?.value?.value} /> {/* TODO(MOB-2166): remove forced LTR direction + scaleX horizontal flip technique once react-native-wagmi-charts fixes this: https://github.com/coinjar/react-native-wagmi-charts/issues/136 */} diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorerAnimatedNumber.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorerAnimatedNumber.tsx index 9f3b6ed4f77..652e4d35036 100644 --- a/apps/mobile/src/components/PriceExplorer/PriceExplorerAnimatedNumber.tsx +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorerAnimatedNumber.tsx @@ -24,22 +24,6 @@ import { TextLoaderWrapper } from 'ui/src/components/text/Text' import { fonts } from 'ui/src/theme' import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' -// if price per token has > 3 numbers before the decimal, start showing decimals in neutral3 -// otherwise, show entire price in neutral1 -const DEEMPHASIZED_DECIMALS_THRESHOLD = 3 - -const getEmphasizedNumberColor = ( - index: number, - commaIndex: number, - emphasizedColor: string, - deemphasizedColor: string -): string => { - if (index >= commaIndex && commaIndex > DEEMPHASIZED_DECIMALS_THRESHOLD) { - return deemphasizedColor - } - return emphasizedColor -} - const NumbersMain = ({ color, backgroundColor, @@ -111,12 +95,6 @@ const RollNumber = ({ currency: FiatCurrencyInfo }): JSX.Element => { const colors = useSporeColors() - const numberColor = getEmphasizedNumberColor( - index, - commaIndex, - colors.neutral1.val, - colors.neutral3.val - ) const animatedDigit = useDerivedValue(() => { const char = chars.value[index - (commaIndex - decimalPlace.value)] @@ -125,8 +103,9 @@ const RollNumber = ({ }, [chars]) const animatedFontStyle = useAnimatedStyle(() => { + const color = index >= commaIndex ? colors.neutral3.val : colors.neutral1.val return { - color: numberColor, + color, } }) @@ -232,7 +211,7 @@ const RollNumber = ({ ]}> = commaIndex ? colors.neutral3.val : colors.neutral1.val} hidePlaceholder={hidePlaceholder} /> diff --git a/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx b/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx index 86b22030c0c..144158d19e6 100644 --- a/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx +++ b/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react' import { I18nManager, StyleSheet, View } from 'react-native' import { - SharedValue, interpolateColor, + SharedValue, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' diff --git a/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap index 2272247fc0b..785a7a26397 100644 --- a/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap +++ b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap @@ -32,6 +32,7 @@ exports[`PriceText renders loading state 1`] = ` { }) it('returns currentSpot if it is provided', async () => { - const spotPrice = makeMutable(1) - const { result } = renderHookWithProviders(useLineChartPrice, { - initialProps: [spotPrice], + const { result, rerender } = renderHookWithProviders(useLineChartPrice, { + initialProps: [1], }) expect(result.current).toEqual({ @@ -122,7 +121,9 @@ describe(useLineChartPrice, () => { shouldAnimate: expect.objectContaining({ value: true }), }) - spotPrice.value = 2 + await act(() => { + rerender([2]) + }) await waitFor(() => { expect(result.current).toEqual({ @@ -149,7 +150,7 @@ describe(useLineChartPrice, () => { it('returns active cursor price even if currentSpot and data are provided', async () => { mockCursorPrice('3') const { result } = renderHookWithProviders(useLineChartPrice, { - initialProps: [makeMutable(4)], + initialProps: [4], }) expect(result.current).toEqual({ @@ -162,7 +163,7 @@ describe(useLineChartPrice, () => { it('updates returned active cursor price when it changes', async () => { mockCursorPrice('1') const { result } = renderHookWithProviders(useLineChartPrice, { - initialProps: [makeMutable(4)], + initialProps: [4], }) expect(result.current).toEqual( diff --git a/apps/mobile/src/components/PriceExplorer/usePrice.tsx b/apps/mobile/src/components/PriceExplorer/usePrice.tsx index 28fc16af3e0..3c93865a4df 100644 --- a/apps/mobile/src/components/PriceExplorer/usePrice.tsx +++ b/apps/mobile/src/components/PriceExplorer/usePrice.tsx @@ -26,9 +26,7 @@ export type ValueAndFormattedWithAnimation = ValueAndFormatted & { * Wrapper around react-native-wagmi-chart#useLineChartPrice * @returns latest price when not scrubbing and active price when scrubbing */ -export function useLineChartPrice( - currentSpot?: SharedValue -): ValueAndFormattedWithAnimation { +export function useLineChartPrice(currentSpot?: number): ValueAndFormattedWithAnimation { const { value: activeCursorPrice } = useRNWagmiChartLineChartPrice({ // do not round precision: 18, @@ -57,7 +55,7 @@ export function useLineChartPrice( shouldAnimate.value = true // show spot price when chart not scrubbing, or if not available, show the last price in the chart - return currentSpot?.value ?? data[data.length - 1]?.value ?? 0 + return currentSpot ?? data[data.length - 1]?.value ?? 0 }) const priceFormatted = useDerivedValue(() => { const { symbol, code } = currencyInfo diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts index fa7da3e30a9..c513bfb4207 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { waitFor } from '@testing-library/react-native' import { act } from 'react-test-renderer' import { useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory' @@ -8,15 +9,15 @@ import { TokenMarket as TokenMarketType, TokenProject as TokenProjectType, } from 'wallet/src/data/__generated__/types-and-hooks' -import { SAMPLE_CURRENCY_ID_1, faker } from 'wallet/src/test/fixtures' +import { faker, SAMPLE_CURRENCY_ID_1 } from 'wallet/src/test/fixtures' import { EthToken, TokenDayPriceHistory, TokenMarket, TokenProjectDay, + TokenProjects, TokenProjectWeek, TokenProjectYear, - TokenProjects, TokenWeekPriceHistory, TokenYearPriceHistory, } from 'wallet/src/test/gqlFixtures' @@ -85,8 +86,8 @@ describe(useTokenPriceHistory, () => { }) expect(result.current.data?.spot).toEqual({ - value: expect.objectContaining({ value: TokenMarket.price?.value }), - relativeChange: expect.objectContaining({ value: TokenMarket.pricePercentChange?.value }), + value: { value: TokenMarket.price?.value }, + relativeChange: { value: TokenMarket.pricePercentChange?.value }, }) }) @@ -215,10 +216,8 @@ describe(useTokenPriceHistory, () => { await waitFor(() => { expect(result.current.data?.spot).toEqual({ - value: expect.objectContaining({ value: TokenProjectDay.markets?.[0]?.price?.value }), - relativeChange: expect.objectContaining({ - value: TokenProjectDay.markets?.[0]?.pricePercentChange24h?.value, - }), + value: { value: TokenProjectDay.markets?.[0]?.price?.value }, + relativeChange: { value: TokenProjectDay.markets?.[0]?.pricePercentChange24h?.value }, }) }) }) @@ -250,10 +249,8 @@ describe(useTokenPriceHistory, () => { await waitFor(() => { expect(result.current.data?.spot).toEqual({ - value: expect.objectContaining({ value: TokenProjectYear.markets?.[0]?.price?.value }), - relativeChange: expect.objectContaining({ - value: TokenProjectYear.markets?.[0]?.pricePercentChange24h?.value, - }), + value: { value: TokenProjectYear.markets?.[0]?.price?.value }, + relativeChange: { value: TokenProjectYear.markets?.[0]?.pricePercentChange24h?.value }, }) }) }) @@ -301,10 +298,8 @@ describe(useTokenPriceHistory, () => { expect(result.current.data).toEqual({ priceHistory: formatPriceHistory(TokenDayPriceHistory), spot: { - value: expect.objectContaining({ value: TokenProjectDay.markets?.[0]?.price?.value }), - relativeChange: expect.objectContaining({ - value: TokenProjectDay.markets?.[0]?.pricePercentChange24h?.value, - }), + value: { value: TokenProjectDay.markets?.[0]?.price?.value }, + relativeChange: { value: TokenProjectDay.markets?.[0]?.pricePercentChange24h?.value }, }, }) }) @@ -318,10 +313,8 @@ describe(useTokenPriceHistory, () => { expect(result.current.data).toEqual({ priceHistory: formatPriceHistory(TokenWeekPriceHistory), spot: { - value: expect.objectContaining({ value: TokenProjectWeek.markets?.[0]?.price?.value }), - relativeChange: expect.objectContaining({ - value: TokenProjectWeek.markets?.[0]?.pricePercentChange24h?.value, - }), + value: { value: TokenProjectWeek.markets?.[0]?.price?.value }, + relativeChange: { value: TokenProjectWeek.markets?.[0]?.pricePercentChange24h?.value }, }, }) }) diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts index 46fbfb34450..1bc98a03da9 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts @@ -1,15 +1,15 @@ import { maxBy } from 'lodash' import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react' -import { SharedValue, useDerivedValue } from 'react-native-reanimated' +import { SharedValue } from 'react-native-reanimated' import { TLineChartData } from 'react-native-wagmi-charts' +import { GqlResult } from 'src/features/dataApi/types' import { PollingInterval } from 'wallet/src/constants/misc' +import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { HistoryDuration, TimestampedAmount, useTokenPriceHistoryQuery, } from 'wallet/src/data/__generated__/types-and-hooks' -import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' -import { GqlResult } from 'wallet/src/features/dataApi/types' import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' @@ -69,18 +69,15 @@ export function useTokenPriceHistory( const pricePercentChange24h = offChainData?.pricePercentChange24h?.value ?? onChainData?.pricePercentChange24h?.value ?? 0 - const spotValue = useDerivedValue(() => price ?? 0) - const spotRelativeChange = useDerivedValue(() => pricePercentChange24h) - const spot = useMemo( () => - price !== undefined + price ? { - value: spotValue, - relativeChange: spotRelativeChange, + value: { value: price }, + relativeChange: { value: pricePercentChange24h }, } : undefined, - [price, spotValue, spotRelativeChange] + [price, pricePercentChange24h] ) const formattedPriceHistory = useMemo(() => { diff --git a/apps/mobile/src/components/QRCodeScanner/QRCode.tsx b/apps/mobile/src/components/QRCodeScanner/QRCode.tsx index 53cbb40ea95..df7311b9a1b 100644 --- a/apps/mobile/src/components/QRCodeScanner/QRCode.tsx +++ b/apps/mobile/src/components/QRCodeScanner/QRCode.tsx @@ -1,7 +1,9 @@ import React, { memo, useMemo } from 'react' import { ImageSourcePropType, StyleSheet } from 'react-native' import QRCode from 'src/components/QRCodeScanner/custom-qr-code-generator' -import { ColorTokens, Flex, Unicon, useSporeColors, useUniconColors } from 'ui/src' +import { Unicon } from 'src/components/unicons/Unicon' +import { useUniconColors } from 'src/components/unicons/utils' +import { ColorTokens, Flex, useSporeColors } from 'ui/src' import { borderRadii, opacify } from 'ui/src/theme' import { isAndroid } from 'wallet/src/utils/platform' diff --git a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx index 1e985703f6a..97fd366b24f 100644 --- a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx +++ b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx @@ -6,7 +6,10 @@ import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-nat import { launchImageLibrary } from 'react-native-image-picker' import { FadeIn, FadeOut } from 'react-native-reanimated' import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' +import PasteButton from 'src/components/buttons/PasteButton' import { DevelopmentOnly } from 'src/components/DevelopmentOnly/DevelopmentOnly' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { openSettings } from 'src/utils/linking' import { AnimatedFlex, Button, @@ -19,9 +22,6 @@ import { import CameraScan from 'ui/src/assets/icons/camera-scan.svg' import { iconSizes, spacing } from 'ui/src/theme' import { useAsyncData } from 'utilities/src/react/hooks' -import PasteButton from 'wallet/src/components/buttons/PasteButton' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { openSettings } from 'wallet/src/utils/linking' type QRCodeScannerProps = { onScanCode: (data: string) => void diff --git a/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx b/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx index db9a5904ee0..31225c51c14 100644 --- a/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx +++ b/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx @@ -1,28 +1,20 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { FadeIn, FadeOut } from 'react-native-reanimated' +import { AddressDisplay } from 'src/components/AddressDisplay' import { GradientBackground } from 'src/components/gradients/GradientBackground' import { UniconThemedGradient } from 'src/components/gradients/UniconThemedGradient' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode' +import { LearnMoreLink } from 'src/components/text/LearnMoreLink' +import { useUniconColors } from 'src/components/unicons/utils' import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' -import { - AnimatedFlex, - Flex, - Icons, - Text, - TouchableArea, - useIsDarkMode, - useMedia, - useSporeColors, - useUniconColors, -} from 'ui/src' +import { ModalName } from 'src/features/telemetry/constants' +import { AnimatedFlex, Flex, Icons, Text, useMedia, useSporeColors } from 'ui/src' import { iconSizes, spacing } from 'ui/src/theme' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { ALL_SUPPORTED_CHAIN_IDS } from 'wallet/src/constants/chains' import { uniswapUrls } from 'wallet/src/constants/urls' -import { ModalName } from 'wallet/src/telemetry/constants' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' interface Props { address?: Address @@ -87,17 +79,16 @@ export function WalletQRCode({ address }: Props): JSX.Element | null { {t('You can send tokens on all of our supported networks to this address.')} - setShowModal(true)}> - - - - - + + + setShowModal(true)} + /> + {showModal && ( ): JSX.Element { return ( - // TODO(EXT-526): re-enable `exiting` animation when it's fixed. - + ) @@ -36,12 +35,7 @@ export function RecipientList({ onPress, sections }: RecipientListProps): JSX.El function SectionHeader(info: { section: SectionListData }): JSX.Element { return ( - + {info.section.title} diff --git a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx index 5d8415efd98..fce8a8f0834 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx @@ -4,17 +4,18 @@ import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' import 'react-native-reanimated' import { useAppSelector } from 'src/app/hooks' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' import { WalletQRCode } from 'src/components/QRCodeScanner/WalletQRCode' import { getSupportedURI, URIType } from 'src/components/WalletConnect/ScanSheet/util' -import { Flex, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' +import { ElementName, ModalName } from 'src/features/telemetry/constants' +import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import Scan from 'ui/src/assets/icons/receive.svg' import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import { iconSizes } from 'ui/src/theme' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' type Props = { onClose: () => void diff --git a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx index c45e0ed5038..8df5e1f9583 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx @@ -2,17 +2,17 @@ import React, { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Keyboard } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' +import { useBottomSheetContext } from 'src/components/modals/BottomSheetContext' +import { filterRecipientByNameAndAddress } from 'src/components/RecipientSelect/filter' +import { useRecipients } from 'src/components/RecipientSelect/hooks' +import { RecipientList } from 'src/components/RecipientSelect/RecipientList' import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal' +import { filterSections } from 'src/components/RecipientSelect/utils' +import { SearchBar } from 'src/components/TokenSelector/SearchBar' +import { ElementName } from 'src/features/telemetry/constants' import { AnimatedFlex, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import { iconSizes } from 'ui/src/theme' -import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' -import { filterRecipientByNameAndAddress } from 'wallet/src/components/RecipientSearch/filter' -import { useRecipients } from 'wallet/src/components/RecipientSearch/hooks' -import { RecipientList } from 'wallet/src/components/RecipientSearch/RecipientList' -import { filterSections } from 'wallet/src/components/RecipientSearch/utils' -import { SearchBar } from 'wallet/src/features/search/SearchBar' -import { ElementName } from 'wallet/src/telemetry/constants' interface RecipientSelectProps { onSelectRecipient: (newRecipientAddress: string) => void diff --git a/packages/wallet/src/components/RecipientSearch/filter.test.ts b/apps/mobile/src/components/RecipientSelect/filter.test.ts similarity index 98% rename from packages/wallet/src/components/RecipientSearch/filter.test.ts rename to apps/mobile/src/components/RecipientSelect/filter.test.ts index 69f49f02f55..b3361a01ce5 100644 --- a/packages/wallet/src/components/RecipientSearch/filter.test.ts +++ b/apps/mobile/src/components/RecipientSelect/filter.test.ts @@ -4,7 +4,7 @@ import { filterRecipientByNameAndAddress, filterRecipientsByAddress, filterRecipientsByName, -} from 'wallet/src/components/RecipientSearch/filter' +} from 'src/components/RecipientSelect/filter' import { SearchableRecipient } from 'wallet/src/features/address/types' import { SearchableRecipients } from 'wallet/src/test/fixtures' diff --git a/packages/wallet/src/components/RecipientSearch/filter.ts b/apps/mobile/src/components/RecipientSelect/filter.ts similarity index 100% rename from packages/wallet/src/components/RecipientSearch/filter.ts rename to apps/mobile/src/components/RecipientSelect/filter.ts diff --git a/apps/mobile/src/components/RecipientSelect/hooks.test.ts b/apps/mobile/src/components/RecipientSelect/hooks.test.ts index 744bcd408a1..743b29d209e 100644 --- a/apps/mobile/src/components/RecipientSelect/hooks.test.ts +++ b/apps/mobile/src/components/RecipientSelect/hooks.test.ts @@ -3,7 +3,8 @@ import { waitFor } from '@testing-library/react-native' import { toIncludeSameMembers } from 'jest-extended' import { act } from 'react-test-renderer' import { MobileState } from 'src/app/reducer' -import { useRecipients } from 'wallet/src/components/RecipientSearch/hooks' +import { useRecipients } from 'src/components/RecipientSelect/hooks' +import { renderHook } from 'src/test/test-utils' import { ChainId } from 'wallet/src/constants/chains' import { SearchableRecipient } from 'wallet/src/features/address/types' import { TransactionStateMap } from 'wallet/src/features/transactions/slice' @@ -18,14 +19,9 @@ import { sendTxDetailsFailed, sendTxDetailsPending, } from 'wallet/src/test/fixtures' -import { renderHook } from 'wallet/src/test/test-utils' expect.extend({ toIncludeSameMembers }) -/** - * Tests interaction of mobile state with useRecipients hook - */ - type PreloadedStateProps = { watchedAddresses?: Address[] hasInactiveAccounts?: boolean @@ -234,6 +230,7 @@ describe(useRecipients, () => { }), }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const section = result.current.sections[0]! expect(section.title).toEqual('Recent') // This method doesn't check the order of the elements diff --git a/packages/wallet/src/components/RecipientSearch/hooks.ts b/apps/mobile/src/components/RecipientSelect/hooks.ts similarity index 98% rename from packages/wallet/src/components/RecipientSearch/hooks.ts rename to apps/mobile/src/components/RecipientSelect/hooks.ts index d0fcd4b1146..82a3ac1f62a 100644 --- a/packages/wallet/src/components/RecipientSearch/hooks.ts +++ b/apps/mobile/src/components/RecipientSelect/hooks.ts @@ -1,5 +1,6 @@ import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useAppSelector } from 'src/app/hooks' import { ChainId } from 'wallet/src/constants/chains' import { SearchableRecipient } from 'wallet/src/features/address/types' import { uniqueAddressesOnly } from 'wallet/src/features/address/utils' @@ -7,7 +8,6 @@ import { useENS } from 'wallet/src/features/ens/useENS' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' import { selectRecipientsByRecency } from 'wallet/src/features/transactions/selectors' import { selectInactiveAccounts } from 'wallet/src/features/wallet/selectors' -import { useAppSelector } from 'wallet/src/state' import { getValidAddress } from 'wallet/src/utils/addresses' const MAX_RECENT_RECIPIENTS = 15 diff --git a/packages/wallet/src/components/RecipientSearch/utils.test.ts b/apps/mobile/src/components/RecipientSelect/utils.test.ts similarity index 94% rename from packages/wallet/src/components/RecipientSearch/utils.test.ts rename to apps/mobile/src/components/RecipientSelect/utils.test.ts index a086de89090..f7b2a63565b 100644 --- a/packages/wallet/src/components/RecipientSearch/utils.test.ts +++ b/apps/mobile/src/components/RecipientSelect/utils.test.ts @@ -1,4 +1,4 @@ -import { filterSections } from 'wallet/src/components/RecipientSearch/utils' +import { filterSections } from 'src/components/RecipientSelect/utils' import { RecipientSections, SAMPLE_SEED_ADDRESS_1, diff --git a/packages/wallet/src/components/RecipientSearch/utils.ts b/apps/mobile/src/components/RecipientSelect/utils.ts similarity index 100% rename from packages/wallet/src/components/RecipientSearch/utils.ts rename to apps/mobile/src/components/RecipientSelect/utils.ts diff --git a/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx b/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx index 3b62016a75d..6e9fc53b80b 100644 --- a/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx +++ b/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx @@ -1,11 +1,11 @@ import React, { useMemo } from 'react' import { ScrollView, StyleSheet } from 'react-native' +import { useAccountList } from 'src/components/accounts/hooks' +import { AddressDisplay } from 'src/components/AddressDisplay' import { Flex, Text, useDeviceDimensions } from 'ui/src' import { spacing } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AccountListQuery } from 'wallet/src/data/__generated__/types-and-hooks' -import { useAccountList } from 'wallet/src/features/accounts/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { Account } from 'wallet/src/features/wallet/accounts/types' diff --git a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx index dcf36b62be9..d985925955d 100644 --- a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx +++ b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { ElementName } from 'src/features/telemetry/constants' import { Button, CheckBox, Flex, Text } from 'ui/src' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { ElementName } from 'wallet/src/telemetry/constants' export function RemoveLastMnemonicWalletFooter({ onPress, diff --git a/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx index 459cfddec0e..6edcf1af70e 100644 --- a/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx +++ b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx @@ -4,6 +4,8 @@ import { useAnimatedStyle, withTiming } from 'react-native-reanimated' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' import { Delay } from 'src/components/layout/Delayed' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { AssociatedAccountsList } from 'src/components/RemoveWallet/AssociatedAccountsList' import { RemoveLastMnemonicWalletFooter } from 'src/components/RemoveWallet/RemoveLastMnemonicWalletFooter' import { RemoveWalletStep, useModalContent } from 'src/components/RemoveWallet/useModalContent' @@ -11,11 +13,10 @@ import { navigateToOnboardingImportMethod } from 'src/components/RemoveWallet/ut import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { OnboardingScreens, Screens } from 'src/screens/Screens' import { AnimatedFlex, Button, ColorTokens, Flex, Text, ThemeKeys, useSporeColors } from 'ui/src' import { iconSizes, opacify } from 'ui/src/theme' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' import { EditAccountAction, @@ -24,7 +25,6 @@ import { import { useAccounts } from 'wallet/src/features/wallet/hooks' import { selectSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' export function RemoveWalletModal(): JSX.Element | null { const { t } = useTranslation() diff --git a/apps/mobile/src/components/RemoveWallet/useModalContent.tsx b/apps/mobile/src/components/RemoveWallet/useModalContent.tsx index 5e55bc9e43b..fbeb5c487da 100644 --- a/apps/mobile/src/components/RemoveWallet/useModalContent.tsx +++ b/apps/mobile/src/components/RemoveWallet/useModalContent.tsx @@ -42,7 +42,7 @@ export const useModalContent = ({ }: ModalContentParams): ModalContentResult | undefined => { const { t } = useTranslation() - const displayName = useDisplayName(account?.address, { includeUnitagSuffix: true }) + const displayName = useDisplayName(account?.address) return useMemo(() => { // 1st speed bump when removing recovery phrase @@ -173,7 +173,7 @@ export const useModalContent = ({ account, associatedAccounts, currentStep, - displayName, + displayName?.name, isRemovingRecoveryPhrase, isReplacing, t, diff --git a/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx b/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx index ee4d9e31956..c7fdc425a00 100644 --- a/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx +++ b/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx @@ -2,14 +2,14 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { useAppDispatch } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { closeAllModals, closeModal } from 'src/features/modals/modalSlice' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { OnboardingScreens, Screens } from 'src/screens/Screens' import { Button, Flex, Text, useSporeColors } from 'ui/src' import LockIcon from 'ui/src/assets/icons/lock.svg' import { iconSizes, opacify } from 'ui/src/theme' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' export function RestoreWalletModal(): JSX.Element | null { const { t } = useTranslation() diff --git a/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx index 93809b8c7e2..94a7693daca 100644 --- a/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx +++ b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx @@ -1,12 +1,9 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal, { WarningModalProps } from 'src/components/modals/WarningModal/WarningModal' import { useBiometricName } from 'src/features/biometrics/hooks' -import { - WarningModal, - WarningModalProps, -} from 'wallet/src/components/modals/WarningModal/WarningModal' -import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' -import { ModalName } from 'wallet/src/telemetry/constants' +import { ModalName } from 'src/features/telemetry/constants' type Props = { isTouchIdDevice: boolean diff --git a/apps/mobile/src/components/Settings/SettingsRow.tsx b/apps/mobile/src/components/Settings/SettingsRow.tsx index 156a215b50b..6569bebb71a 100644 --- a/apps/mobile/src/components/Settings/SettingsRow.tsx +++ b/apps/mobile/src/components/Settings/SettingsRow.tsx @@ -7,15 +7,15 @@ import { SettingsStackNavigationProp, SettingsStackParamList, } from 'src/app/navigation/types' +import { Switch } from 'src/components/buttons/Switch' +import { Arrow } from 'src/components/icons/Arrow' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' +import { openUri } from 'src/utils/linking' import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { Switch } from 'wallet/src/components/buttons/Switch' -import { Arrow } from 'wallet/src/components/icons/Arrow' import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' -import { openUri } from 'wallet/src/utils/linking' export interface SettingsSection { subTitle: string @@ -27,8 +27,7 @@ export interface SettingsSectionItemComponent { component: JSX.Element isHidden?: boolean } -type SettingsModal = typeof ModalName.FiatCurrencySelector | typeof ModalName.LanguageSelector - +type SettingsModal = Extract export interface SettingsSectionItem { screen?: keyof SettingsStackParamList | typeof Screens.OnboardingStack modal?: SettingsModal diff --git a/packages/wallet/src/features/transactions/TransactionDetails/SwapFee.tsx b/apps/mobile/src/components/SwapFee/SwapFee.tsx similarity index 98% rename from packages/wallet/src/features/transactions/TransactionDetails/SwapFee.tsx rename to apps/mobile/src/components/SwapFee/SwapFee.tsx index a5cdf073b53..927bc5a7502 100644 --- a/packages/wallet/src/features/transactions/TransactionDetails/SwapFee.tsx +++ b/apps/mobile/src/components/SwapFee/SwapFee.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { useTranslation } from 'react-i18next' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { NumberType } from 'utilities/src/format/types' diff --git a/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx b/apps/mobile/src/components/TokenBalanceList/HiddenTokensRow.tsx similarity index 85% rename from packages/wallet/src/features/portfolio/HiddenTokensRow.tsx rename to apps/mobile/src/components/TokenBalanceList/HiddenTokensRow.tsx index 180cf8d8bdd..6426f7904aa 100644 --- a/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx +++ b/apps/mobile/src/components/TokenBalanceList/HiddenTokensRow.tsx @@ -1,31 +1,28 @@ import { ImpactFeedbackStyle } from 'expo-haptics' -import { useCallback } from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated' import { AnimatedFlex, Flex, Icons, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' export function HiddenTokensRow({ - padded = false, numHidden, isExpanded, onPress, }: { - padded?: boolean numHidden: number isExpanded: boolean onPress: () => void }): JSX.Element { const { t } = useTranslation() - // TODO (EXT-482): make chevron rotation work with Tamagui animation const chevronRotate = useSharedValue(isExpanded ? 180 : 0) const chevronAnimatedStyle = useAnimatedStyle(() => { return { transform: [{ rotateZ: `${chevronRotate.value}deg` }], } - }, []) + }) const onPressRow = useCallback(() => { chevronRotate.value = withTiming(chevronRotate.value === 0 ? 180 : 0, { @@ -37,12 +34,7 @@ export function HiddenTokensRow({ return ( - + {t('Hidden ({{numHidden}})', { numHidden })} diff --git a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx index 61c58113bdf..b408e24051f 100644 --- a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx +++ b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx @@ -8,31 +8,25 @@ import Animated, { FadeInDown, FadeOut } from 'react-native-reanimated' import { useAppStackNavigation } from 'src/app/navigation/types' import { useAdaptiveFooter } from 'src/components/home/hooks' import { + TabProps, TAB_BAR_HEIGHT, TAB_VIEW_SCROLL_THROTTLE, - TabProps, } from 'src/components/layout/TabHelpers' +import { Loader } from 'src/components/loading' +import { HiddenTokensRow } from 'src/components/TokenBalanceList/HiddenTokensRow' import { TokenBalanceItemContextMenu } from 'src/components/TokenBalanceList/TokenBalanceItemContextMenu' -import { Screens } from 'src/screens/Screens' -import { - AnimatedFlex, - Flex, - Loader, - useDeviceDimensions, - useDeviceInsets, - useSporeColors, -} from 'ui/src' -import { zIndices } from 'ui/src/theme' -import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' -import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' -import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow' -import { TokenBalanceItem } from 'wallet/src/features/portfolio/TokenBalanceItem' import { HIDDEN_TOKEN_BALANCES_ROW, TokenBalanceListContextProvider, TokenBalanceListRow, useTokenBalanceListContext, -} from 'wallet/src/features/portfolio/TokenBalanceListContext' +} from 'src/components/TokenBalanceList/TokenBalanceListContext' +import { Screens } from 'src/screens/Screens' +import { AnimatedFlex, Flex, useDeviceDimensions, useDeviceInsets, useSporeColors } from 'ui/src' +import { zIndices } from 'ui/src/theme' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' +import { TokenBalanceItem } from 'wallet/src/features/portfolio/TokenBalanceItem' import { CurrencyId } from 'wallet/src/utils/currencyId' import { isAndroid } from 'wallet/src/utils/platform' @@ -172,7 +166,9 @@ export const TokenBalanceListInner = forwardRef< // add negative z index to prevent footer from covering hidden tokens row when minimized const ListFooterComponentStyle = useMemo(() => ({ zIndex: zIndices.negative }), []) - const List = renderedInModal ? BottomSheetFlatList : Animated.FlatList + const List = renderedInModal + ? BottomSheetFlatList + : Animated.FlatList const getItemLayout = useCallback( ( @@ -260,7 +256,6 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ if (item === HIDDEN_TOKEN_BALANCES_ROW) { return ( { diff --git a/packages/wallet/src/features/portfolio/TokenBalanceListContext.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceListContext.tsx similarity index 100% rename from packages/wallet/src/features/portfolio/TokenBalanceListContext.tsx rename to apps/mobile/src/components/TokenBalanceList/TokenBalanceListContext.tsx diff --git a/apps/mobile/src/components/TokenDetails/LinkButton.tsx b/apps/mobile/src/components/TokenDetails/LinkButton.tsx index b7018422682..2d5ad6f2b82 100644 --- a/apps/mobile/src/components/TokenDetails/LinkButton.tsx +++ b/apps/mobile/src/components/TokenDetails/LinkButton.tsx @@ -2,14 +2,14 @@ import React from 'react' import { SvgProps } from 'react-native-svg' import { useAppDispatch } from 'src/app/hooks' import Trace from 'src/components/Trace/Trace' +import { ElementName } from 'src/features/telemetry/constants' +import { setClipboard } from 'src/utils/clipboard' +import { openUri } from 'src/utils/linking' import { Flex, IconProps, Text, TouchableArea, useSporeColors } from 'ui/src' import CopyIcon from 'ui/src/assets/icons/copy-sheets.svg' import { iconSizes } from 'ui/src/theme' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' -import { ElementNameType } from 'wallet/src/telemetry/constants' -import { setClipboard } from 'wallet/src/utils/clipboard' -import { openUri } from 'wallet/src/utils/linking' export enum LinkButtonType { Copy = 'copy', @@ -28,7 +28,7 @@ export function LinkButton({ buttonType: LinkButtonType label: string Icon?: React.FC - element: ElementNameType + element: ElementName openExternalBrowser?: boolean isSafeUri?: boolean value: string diff --git a/apps/mobile/src/components/TokenDetails/SendButton.tsx b/apps/mobile/src/components/TokenDetails/SendButton.tsx index 40bc2d50e26..030ae495975 100644 --- a/apps/mobile/src/components/TokenDetails/SendButton.tsx +++ b/apps/mobile/src/components/TokenDetails/SendButton.tsx @@ -1,8 +1,8 @@ import React from 'react' +import { ElementName } from 'src/features/telemetry/constants' import { Flex, TouchableArea } from 'ui/src' import SendIcon from 'ui/src/assets/icons/send-action.svg' import { iconSizes } from 'ui/src/theme' -import { ElementName } from 'wallet/src/telemetry/constants' type Props = { onPress: () => void diff --git a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx index 3de1ccb1065..c97173d7b24 100644 --- a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx @@ -1,5 +1,6 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { InlineNetworkPill } from 'src/components/Network/NetworkPill' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import Trace from 'src/components/Trace/Trace' import { MobileEventName } from 'src/features/telemetry/constants' @@ -7,7 +8,6 @@ import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' -import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill' import { PortfolioBalance } from 'wallet/src/features/dataApi/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { AccountType } from 'wallet/src/features/wallet/accounts/types' @@ -33,7 +33,7 @@ export function TokenBalances({ const activeAccount = useActiveAccount() const accountType = activeAccount?.type - const displayName = useDisplayName(activeAccount?.address, { includeUnitagSuffix: true })?.name + const displayName = useDisplayName(activeAccount?.address)?.name const isReadonly = accountType === AccountType.Readonly const hasCurrentChainBalances = Boolean(currentChainBalance) diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx index ed3c0a4b31c..59cfc3d3d67 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx @@ -1,9 +1,9 @@ import React from 'react' import { useTranslation } from 'react-i18next' import Trace from 'src/components/Trace/Trace' +import { ElementName, SectionName } from 'src/features/telemetry/constants' import { Button, Flex } from 'ui/src' import { validColor } from 'ui/src/theme' -import { ElementName, ElementNameType, SectionName } from 'wallet/src/telemetry/constants' import { getContrastPassingTextColor } from 'wallet/src/utils/colors' function CTAButton({ @@ -13,7 +13,7 @@ function CTAButton({ tokenColor, }: { title: string - element: ElementNameType + element: ElementName onPress: () => void tokenColor?: Maybe }): JSX.Element { diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx index 2f44731dde9..96536c41d79 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx @@ -1,8 +1,8 @@ import React from 'react' +import WarningIcon from 'src/components/tokens/WarningIcon' import { Flex, flexStyles, Text, TouchableArea } from 'ui/src' import { iconSizes, imageSizes } from 'ui/src/theme' import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' -import WarningIcon from 'wallet/src/components/icons/WarningIcon' import { SafetyLevel, TokenDetailsScreenQuery } from 'wallet/src/data/__generated__/types-and-hooks' import { fromGraphQLChain } from 'wallet/src/features/chains/utils' diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx index cb0d2c11231..10190a89090 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx @@ -2,18 +2,16 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { ScrollView, View } from 'react-native' import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' +import { ElementName } from 'src/features/telemetry/constants' +import { ExplorerDataType, getExplorerLink, getTwitterLink } from 'src/utils/linking' import { Flex, Text } from 'ui/src' import GlobeIcon from 'ui/src/assets/icons/globe-filled.svg' -import TwitterIcon from 'ui/src/assets/icons/x-twitter.svg' -import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import AddressIcon from 'ui/src/assets/icons/sticky-note-text-square.svg' +import TwitterIcon from 'ui/src/assets/icons/twitter.svg' +import { ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' import { TokenDetailsScreenQuery } from 'wallet/src/data/__generated__/types-and-hooks' -import { ElementName } from 'wallet/src/telemetry/constants' -import { - currencyIdToAddress, - currencyIdToChain, - isDefaultNativeAddress, -} from 'wallet/src/utils/currencyId' -import { ExplorerDataType, getExplorerLink, getTwitterLink } from 'wallet/src/utils/linking' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' +import { currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' import { LinkButton, LinkButtonType } from './LinkButton' export function TokenDetailsLinks({ @@ -29,7 +27,6 @@ export function TokenDetailsLinks({ const chainId = currencyIdToChain(currencyId) ?? ChainId.Mainnet const address = currencyIdToAddress(currencyId) const explorerLink = getExplorerLink(chainId, address, ExplorerDataType.TOKEN) - const explorerName = CHAIN_INFO[chainId].explorer.name return ( // eslint-disable-next-line react-native/no-inline-styles @@ -41,11 +38,11 @@ export function TokenDetailsLinks({ {homepageUrl && ( )} - {!isDefaultNativeAddress(address) && ( - - )} + diff --git a/apps/mobile/src/components/TokenDetails/hooks.test.ts b/apps/mobile/src/components/TokenDetails/hooks.test.ts deleted file mode 100644 index fe994bb32d6..00000000000 --- a/apps/mobile/src/components/TokenDetails/hooks.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { useCrossChainBalances, useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' -import { Screens } from 'src/screens/Screens' -import { act, renderHook, waitFor } from 'src/test/test-utils' -import { USDBC_BASE, USDC_ARBITRUM } from 'wallet/src/constants/tokens' -import { Chain } from 'wallet/src/data/__generated__/types-and-hooks' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' -import { SAMPLE_CURRENCY_ID_1, mockWalletPreloadedState } from 'wallet/src/test/fixtures' -import { Portfolio, Portfolio2, PortfolioBalancesById } from 'wallet/src/test/gqlFixtures' - -const mockedNavigation = { - navigate: jest.fn(), - canGoBack: jest.fn(), - pop: jest.fn(), - push: jest.fn(), -} - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native') - return { - ...actualNav, - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - useNavigation: () => mockedNavigation, - } -}) - -describe(useCrossChainBalances, () => { - describe('currentChainBalance', () => { - it('returns null if there are no balances for the specified currency', async () => { - const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), { - preloadedState: mockWalletPreloadedState, - }) - - await act(() => undefined) - - expect(result.current).toEqual( - expect.objectContaining({ - currentChainBalance: null, - }) - ) - }) - - it('returns balance if there is at least one for the specified currency', async () => { - const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), { - preloadedState: mockWalletPreloadedState, - resolvers: { - Query: { - portfolios: () => [Portfolio], - }, - }, - }) - - await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ - currentChainBalance: PortfolioBalancesById[SAMPLE_CURRENCY_ID_1], - }) - ) - }) - }) - }) - - describe('otherChainBalances', () => { - // Current chain balance will be determined by the following currency id - const currencyId1 = `${fromGraphQLChain(Chain.Base)}-${USDBC_BASE.address.toLocaleLowerCase()}` - const currencyId2 = `${fromGraphQLChain( - Chain.Arbitrum - )}-${USDC_ARBITRUM.address.toLocaleLowerCase()}` - - it('returns null if there are no bridged currencies', async () => { - const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), { - preloadedState: mockWalletPreloadedState, - }) - - await act(() => undefined) - - expect(result.current).toEqual( - expect.objectContaining({ - otherChainBalances: null, - }) - ) - }) - - it('does not include current chain balance in other chain balances', async () => { - const bridgeInfo: { chain: Chain; address?: string }[] = [ - { chain: Chain.Base, address: USDBC_BASE.address.toLocaleLowerCase() }, - { chain: Chain.Arbitrum, address: USDC_ARBITRUM.address.toLocaleLowerCase() }, - ] - const { result } = renderHook(() => useCrossChainBalances(currencyId1, bridgeInfo), { - preloadedState: mockWalletPreloadedState, - resolvers: { - Query: { - portfolios: () => [Portfolio2], - }, - }, - }) - - await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ - currentChainBalance: PortfolioBalancesById[currencyId1], - otherChainBalances: [PortfolioBalancesById[currencyId2]], - }) - ) - }) - }) - }) -}) - -describe(useTokenDetailsNavigation, () => { - afterEach(() => { - jest.resetAllMocks() - }) - - it('returns correct result', () => { - const { result } = renderHook(() => useTokenDetailsNavigation()) - - expect(result.current).toEqual({ - preload: expect.any(Function), - navigate: expect.any(Function), - navigateWithPop: expect.any(Function), - }) - }) - - it('preloads token details when preload function is called', async () => { - const queryResolver = jest.fn() - const { result } = renderHook(() => useTokenDetailsNavigation(), { - resolvers: { - Query: { - token: queryResolver, - }, - }, - }) - - await act(() => result.current.preload(SAMPLE_CURRENCY_ID_1)) - - expect(queryResolver).toHaveBeenCalledTimes(1) - expect(queryResolver.mock.calls[0][1]).toEqual(currencyIdToContractInput(SAMPLE_CURRENCY_ID_1)) - }) - - it('navigates to token details when navigate function is called', async () => { - const { result } = renderHook(() => useTokenDetailsNavigation()) - - await act(() => result.current.navigate(SAMPLE_CURRENCY_ID_1)) - - expect(mockedNavigation.navigate).toHaveBeenCalledTimes(1) - expect(mockedNavigation.navigate).toHaveBeenNthCalledWith(1, Screens.TokenDetails, { - currencyId: SAMPLE_CURRENCY_ID_1, - }) - }) - - describe('navigationWithPop', () => { - it('pops the last screen from the stack and navigates to token details if can go back', async () => { - mockedNavigation.canGoBack.mockReturnValueOnce(true) - const { result } = renderHook(() => useTokenDetailsNavigation()) - - await act(() => result.current.navigateWithPop(SAMPLE_CURRENCY_ID_1)) - - expect(mockedNavigation.pop).toHaveBeenCalledTimes(1) - expect(mockedNavigation.push).toHaveBeenCalledTimes(1) - expect(mockedNavigation.push).toHaveBeenNthCalledWith(1, Screens.TokenDetails, { - currencyId: SAMPLE_CURRENCY_ID_1, - }) - }) - - it('pushes token details screen to the stack without popping if there is no previous screen', async () => { - mockedNavigation.canGoBack.mockReturnValueOnce(false) - const { result } = renderHook(() => useTokenDetailsNavigation()) - - await act(() => result.current.navigateWithPop(SAMPLE_CURRENCY_ID_1)) - - expect(mockedNavigation.pop).not.toHaveBeenCalled() - expect(mockedNavigation.push).toHaveBeenCalledTimes(1) - expect(mockedNavigation.push).toHaveBeenNthCalledWith(1, Screens.TokenDetails, { - currencyId: SAMPLE_CURRENCY_ID_1, - }) - }) - }) -}) diff --git a/apps/mobile/src/components/TokenDetails/hooks.ts b/apps/mobile/src/components/TokenDetails/hooks.ts index fe6b4da12f7..3cebca145ee 100644 --- a/apps/mobile/src/components/TokenDetails/hooks.ts +++ b/apps/mobile/src/components/TokenDetails/hooks.ts @@ -10,9 +10,9 @@ import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { PortfolioBalance } from 'wallet/src/features/dataApi/types' import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { - CurrencyId, buildCurrencyId, buildNativeCurrencyId, + CurrencyId, currencyIdToChain, } from 'wallet/src/utils/currencyId' diff --git a/packages/wallet/src/components/network/NetworkFilter.tsx b/apps/mobile/src/components/TokenSelector/NetworkFilter.tsx similarity index 93% rename from packages/wallet/src/components/network/NetworkFilter.tsx rename to apps/mobile/src/components/TokenSelector/NetworkFilter.tsx index d7f6f43563b..b9ea4905467 100644 --- a/packages/wallet/src/components/network/NetworkFilter.tsx +++ b/apps/mobile/src/components/TokenSelector/NetworkFilter.tsx @@ -1,19 +1,18 @@ import { ImpactFeedbackStyle, selectionAsync } from 'expo-haptics' -import { useCallback, useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Keyboard, LayoutAnimation, StyleSheet, VirtualizedList } from 'react-native' -import { isWeb } from 'tamagui' +import { ActionSheetModal } from 'src/components/modals/ActionSheetModal' +import { useNetworkOptions } from 'src/components/Network/hooks' +import { ModalName } from 'src/features/telemetry/constants' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import EllipsisIcon from 'ui/src/assets/icons/ellipsis.svg' import { colors, iconSizes } from 'ui/src/theme' import { - SQUARE_BORDER_RADIUS as NETWORK_LOGO_SQUARE_BORDER_RADIUS, NetworkLogo, + SQUARE_BORDER_RADIUS as NETWORK_LOGO_SQUARE_BORDER_RADIUS, } from 'wallet/src/components/CurrencyLogo/NetworkLogo' -import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal' -import { useNetworkOptions } from 'wallet/src/components/network/hooks' import { ChainId } from 'wallet/src/constants/chains' -import { ModalName } from 'wallet/src/telemetry/constants' const ELLIPSIS = 'ellipsis' const NETWORK_ICON_SIZE = iconSizes.icon20 @@ -110,9 +109,7 @@ export function NetworkFilter({ const onPress = useCallback( async (chainId: ChainId | null) => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - if (!isWeb) { - await selectionAsync() - } + await selectionAsync() setShowModal(false) if (showEllipsisIcon && chainId !== selectedChain) { setShowEllipsisIcon(false) diff --git a/packages/wallet/src/features/search/SearchBar.tsx b/apps/mobile/src/components/TokenSelector/SearchBar.tsx similarity index 80% rename from packages/wallet/src/features/search/SearchBar.tsx rename to apps/mobile/src/components/TokenSelector/SearchBar.tsx index 61ba7b8d827..f4c9f7dd523 100644 --- a/packages/wallet/src/features/search/SearchBar.tsx +++ b/apps/mobile/src/components/TokenSelector/SearchBar.tsx @@ -1,7 +1,8 @@ +import React from 'react' +import { SearchTextInput, SearchTextInputProps } from 'src/components/input/SearchTextInput' +import { ElementName } from 'src/features/telemetry/constants' import { Flex, Icons, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { SearchTextInput, SearchTextInputProps } from 'wallet/src/features/search/SearchTextInput' -import { ElementName } from 'wallet/src/telemetry/constants' interface SearchBarProps extends SearchTextInputProps { onBack?: () => void diff --git a/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx b/apps/mobile/src/components/TokenSelector/SelectTokenButton.tsx similarity index 99% rename from packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx rename to apps/mobile/src/components/TokenSelector/SelectTokenButton.tsx index f027ba7d04b..2063112a1bc 100644 --- a/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx +++ b/apps/mobile/src/components/TokenSelector/SelectTokenButton.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { useTranslation } from 'react-i18next' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' diff --git a/packages/wallet/src/components/TokenSelector/SuggestedToken.tsx b/apps/mobile/src/components/TokenSelector/SuggestedToken.tsx similarity index 93% rename from packages/wallet/src/components/TokenSelector/SuggestedToken.tsx rename to apps/mobile/src/components/TokenSelector/SuggestedToken.tsx index 024280f3ce7..85b2fd6fdaf 100644 --- a/packages/wallet/src/components/TokenSelector/SuggestedToken.tsx +++ b/apps/mobile/src/components/TokenSelector/SuggestedToken.tsx @@ -1,14 +1,14 @@ import { ImpactFeedbackStyle } from 'expo-haptics' import { memo } from 'react' -import { TouchableArea, useSporeColors } from 'ui/src' -import { iconSizes } from 'ui/src/theme' -import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' -import { Pill } from 'wallet/src/components/text/Pill' +import { Pill } from 'src/components/text/Pill' import { OnSelectCurrency, SuggestedTokenSection, TokenOption, -} from 'wallet/src/components/TokenSelector/types' +} from 'src/components/TokenSelector/types' +import { TouchableArea, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' import { getSymbolDisplayText } from 'wallet/src/utils/currency' function _SuggestedToken({ diff --git a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx index 235867b4258..bb8f139f822 100644 --- a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx +++ b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx @@ -2,12 +2,13 @@ import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import React, { memo, useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { ListRenderItemInfo } from 'react-native' +import { Loader } from 'src/components/loading' +import { TokenOptionItem } from 'src/components/TokenSelector/TokenOptionItem' import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' -import { Flex, Icons, Inset, Loader, Text, TouchableArea } from 'ui/src' +import { ElementName } from 'src/features/telemetry/constants' +import { Flex, Icons, Inset, Text, TouchableArea } from 'ui/src' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' -import { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem' import { ChainId } from 'wallet/src/constants/chains' -import { ElementName } from 'wallet/src/telemetry/constants' import { CurrencyId } from 'wallet/src/utils/currencyId' interface Props { diff --git a/packages/wallet/src/components/TokenSelector/TokenOptionItem.tsx b/apps/mobile/src/components/TokenSelector/TokenOptionItem.tsx similarity index 92% rename from packages/wallet/src/components/TokenSelector/TokenOptionItem.tsx rename to apps/mobile/src/components/TokenSelector/TokenOptionItem.tsx index 65c37c335d3..13b135cbc98 100644 --- a/packages/wallet/src/components/TokenSelector/TokenOptionItem.tsx +++ b/apps/mobile/src/components/TokenSelector/TokenOptionItem.tsx @@ -1,17 +1,17 @@ import { ImpactFeedbackStyle } from 'expo-haptics' import React, { useCallback, useState } from 'react' import { Keyboard } from 'react-native' +import { InlineNetworkPill } from 'src/components/Network/NetworkPill' +import TokenWarningModal from 'src/components/tokens/TokenWarningModal' +import WarningIcon from 'src/components/tokens/WarningIcon' +import { TokenOption } from 'src/components/TokenSelector/types' +import { useTokenWarningDismissed } from 'src/features/tokens/safetyHooks' import { Flex, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' -import WarningIcon from 'wallet/src/components/icons/WarningIcon' -import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill' -import { TokenOption } from 'wallet/src/components/TokenSelector/types' import { SafetyLevel } from 'wallet/src/data/__generated__/types-and-hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { useTokenWarningDismissed } from 'wallet/src/features/tokens/safetyHooks' -import TokenWarningModal from 'wallet/src/features/tokens/TokenWarningModal' import { shortenAddress } from 'wallet/src/utils/addresses' import { getSymbolDisplayText } from 'wallet/src/utils/currency' @@ -67,7 +67,6 @@ function _TokenOptionItem({ hapticStyle={ImpactFeedbackStyle.Light} opacity={showWarnings && safetyLevel === SafetyLevel.Blocked ? 0.5 : 1} testID={`token-option-${currency.chainId}-${currency.symbol}`} - width="100%" onPress={onPressTokenOption}> void onSelectCurrency: ( currency: Currency, @@ -50,27 +53,12 @@ interface BaseTokenSelectorProps { ) => void } -type TokenSelectorProps = BaseTokenSelectorProps & - ( - | { - onSendEmptyActionPress: () => void - variation: TokenSelectorVariation.BalancesOnly - } - | { - onSendEmptyActionPress?: never - variation: - | TokenSelectorVariation.BalancesAndPopular - | TokenSelectorVariation.SuggestedAndFavoritesAndPopular - } - ) - function TokenSelectorContent({ currencyField, flow, onSelectCurrency, chainId, onClose, - onSendEmptyActionPress, variation, }: TokenSelectorProps): JSX.Element { const { isSheetReady } = useBottomSheetContext() @@ -130,71 +118,6 @@ function TokenSelectorContent({ const [searchInFocus, setSearchInFocus] = useState(false) - const onSendEmptyActionPressWithClose = useCallback(() => { - onClose() - if (!onSendEmptyActionPress) { - logger.error( - new Error( - 'Invalid call to `onSendEmptyActionPressWithClose` with missing `onSendEmptyActionPress`' - ), - { tags: { file: 'TokenSelector.tsx', function: 'onSendEmptyActionPressWithClose' } } - ) - return - } - onSendEmptyActionPress() - }, [onClose, onSendEmptyActionPress]) - - const tokenSelector = useMemo(() => { - if (searchInFocus && !searchFilter) { - return - } - - if (searchFilter) { - return ( - - ) - } - - switch (variation) { - case TokenSelectorVariation.BalancesOnly: - return ( - - ) - case TokenSelectorVariation.BalancesAndPopular: - return ( - - ) - case TokenSelectorVariation.SuggestedAndFavoritesAndPopular: - return ( - - ) - } - }, [ - searchInFocus, - searchFilter, - variation, - onSelectCurrencyCallback, - chainFilter, - debouncedSearchFilter, - onSendEmptyActionPressWithClose, - ]) - return ( @@ -212,8 +135,33 @@ function TokenSelectorContent({ {isSheetReady && ( - {tokenSelector} - + {searchInFocus && !searchFilter ? ( + + ) : searchFilter ? ( + + ) : variation === TokenSelectorVariation.BalancesOnly ? ( + + ) : variation === TokenSelectorVariation.BalancesAndPopular ? ( + + ) : variation === TokenSelectorVariation.SuggestedAndFavoritesAndPopular ? ( + + ) : null} {(!searchInFocus || searchFilter) && ( function isSuggestedTokenItem(data: TokenOption | TokenOption[]): data is TokenOption[] { return Array.isArray(data) @@ -95,7 +94,8 @@ function _TokenSelectorList({ showTokenAddress, }: TokenSelectorListProps): JSX.Element { const { t } = useTranslation() - const sectionListRef = useRef() + const insets = useDeviceInsets() + const sectionListRef = useRef>(null) useEffect(() => { if (sections?.length) { @@ -170,9 +170,7 @@ function _TokenSelectorList({ return ( - - - + @@ -180,16 +178,22 @@ function _TokenSelectorList({ } return ( - // TODO(EXT-526): re-enable `exiting` animation when it's fixed. - - + + ref={sectionListRef} ListEmptyComponent={emptyElement} + bounces={true} + contentContainerStyle={{ paddingBottom: insets.bottom }} focusHook={useBottomSheetFocusHook} keyExtractor={key} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="always" renderItem={renderItem} renderSectionHeader={renderSectionHeader} - sectionListRef={sectionListRef} sections={sections ?? []} + showsVerticalScrollIndicator={false} + stickySectionHeadersEnabled={true} + windowSize={4} /> ) diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx b/apps/mobile/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx similarity index 87% rename from packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx rename to apps/mobile/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx index e538d33bf9d..a3dba8a35dc 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx +++ b/apps/mobile/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx @@ -1,26 +1,20 @@ -import { memo, useCallback, useMemo } from 'react' +import React, { memo, useCallback, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { Flex, Text } from 'ui/src' +import { useAppDispatch } from 'src/app/hooks' import { usePortfolioBalancesForAddressById, usePortfolioTokenOptions, -} from 'wallet/src/components/TokenSelector/hooks' -import { - SectionHeader, - TokenSelectorList, -} from 'wallet/src/components/TokenSelector/TokenSelectorList' -import { OnSelectCurrency, TokenSection } from 'wallet/src/components/TokenSelector/types' -import { - formatSearchResults, - getTokenOptionsSection, -} from 'wallet/src/components/TokenSelector/utils' +} from 'src/components/TokenSelector/hooks' +import { SectionHeader, TokenSelectorList } from 'src/components/TokenSelector/TokenSelectorList' +import { OnSelectCurrency, TokenSection } from 'src/components/TokenSelector/types' +import { formatSearchResults, getTokenOptionsSection } from 'src/components/TokenSelector/utils' +import { useSearchTokens } from 'src/features/dataApi/searchTokens' +import { addToSearchHistory } from 'src/features/explore/searchHistorySlice' +import { SearchResultType } from 'src/features/explore/SearchResult' +import { Flex, Text } from 'ui/src' import { ChainId } from 'wallet/src/constants/chains' -import { useSearchTokens } from 'wallet/src/features/dataApi/searchTokens' import { GqlResult } from 'wallet/src/features/dataApi/types' -import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { SearchResultType } from 'wallet/src/features/search/SearchResult' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' function EmptyResults({ searchFilter }: { searchFilter: string }): JSX.Element { const { t } = useTranslation() diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx b/apps/mobile/src/components/TokenSelector/TokenSelectorSendList.tsx similarity index 69% rename from packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx rename to apps/mobile/src/components/TokenSelector/TokenSelectorSendList.tsx index a4ac81f6864..fcf918b251f 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx +++ b/apps/mobile/src/components/TokenSelector/TokenSelectorSendList.tsx @@ -1,19 +1,21 @@ -import { memo, useMemo } from 'react' +import React, { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { usePortfolioTokenOptions } from 'src/components/TokenSelector/hooks' +import { SectionHeader, TokenSelectorList } from 'src/components/TokenSelector/TokenSelectorList' +import { getTokenOptionsSection } from 'src/components/TokenSelector/utils' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' + +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { OnSelectCurrency, TokenSection } from 'src/components/TokenSelector/types' +import { closeModal, openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { Flex } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { usePortfolioTokenOptions } from 'wallet/src/components/TokenSelector/hooks' -import { - SectionHeader, - TokenSelectorList, -} from 'wallet/src/components/TokenSelector/TokenSelectorList' -import { OnSelectCurrency, TokenSection } from 'wallet/src/components/TokenSelector/types' -import { getTokenOptionsSection } from 'wallet/src/components/TokenSelector/utils' import { ChainId } from 'wallet/src/constants/chains' import { GqlResult } from 'wallet/src/features/dataApi/types' -import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' function useTokenSectionsForSend(chainFilter: ChainId | null): GqlResult { @@ -46,12 +48,28 @@ function useTokenSectionsForSend(chainFilter: ChainId | null): GqlResult void }): JSX.Element { +function EmptyList({ onClose }: { onClose: () => void }): JSX.Element { const { t } = useTranslation() const { data: ipAddressData, isLoading } = useFiatOnRampIpAddressQuery() + const dispatch = useAppDispatch() const fiatOnRampEligible = Boolean(ipAddressData?.isBuyAllowed) + const onEmptyActionPress = (): void => { + onClose() + dispatch(closeModal({ name: ModalName.Send })) + if (fiatOnRampEligible) { + dispatch(openModal({ name: ModalName.FiatOnRamp })) + } else { + dispatch( + openModal({ + name: ModalName.WalletConnectScan, + initialState: ScannerModalState.WalletQr, + }) + ) + } + } + return ( @@ -80,17 +98,14 @@ function EmptyList({ onEmptyActionPress }: { onEmptyActionPress: () => void }): function _TokenSelectorSendList({ onSelectCurrency, chainFilter, - onEmptyActionPress, + onClose, }: { onSelectCurrency: OnSelectCurrency chainFilter: ChainId | null - onEmptyActionPress: () => void + onClose: () => void }): JSX.Element { const { data: sections, loading, error, refetch } = useTokenSectionsForSend(chainFilter) - const emptyElement = useMemo( - () => , - [onEmptyActionPress] - ) + const emptyElement = useMemo(() => , [onClose]) return ( = { diff --git a/packages/wallet/src/components/TokenSelector/flowToModalName.tsx b/apps/mobile/src/components/TokenSelector/flowToModalName.tsx similarity index 58% rename from packages/wallet/src/components/TokenSelector/flowToModalName.tsx rename to apps/mobile/src/components/TokenSelector/flowToModalName.tsx index b094718d73b..bcd6f8ba07e 100644 --- a/packages/wallet/src/components/TokenSelector/flowToModalName.tsx +++ b/apps/mobile/src/components/TokenSelector/flowToModalName.tsx @@ -1,7 +1,7 @@ -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' -import { ModalName, ModalNameType } from 'wallet/src/telemetry/constants' +import { TokenSelectorFlow } from 'src/components/TokenSelector/types' +import { ModalName } from 'src/features/telemetry/constants' -export function flowToModalName(flow: TokenSelectorFlow): ModalNameType | undefined { +export function flowToModalName(flow: TokenSelectorFlow): ModalName | undefined { switch (flow) { case TokenSelectorFlow.Swap: return ModalName.Swap diff --git a/packages/wallet/src/components/TokenSelector/hooks.tsx b/apps/mobile/src/components/TokenSelector/hooks.tsx similarity index 92% rename from packages/wallet/src/components/TokenSelector/hooks.tsx rename to apps/mobile/src/components/TokenSelector/hooks.tsx index 4329faddda6..793ef3e26d6 100644 --- a/packages/wallet/src/components/TokenSelector/hooks.tsx +++ b/apps/mobile/src/components/TokenSelector/hooks.tsx @@ -1,8 +1,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { filter } from 'wallet/src/components/TokenSelector/filter' -import { flowToModalName } from 'wallet/src/components/TokenSelector/flowToModalName' -import { TokenOption } from 'wallet/src/components/TokenSelector/types' -import { createEmptyBalanceOption } from 'wallet/src/components/TokenSelector/utils' +import { useAppSelector } from 'src/app/hooks' +import { filter } from 'src/components/TokenSelector/filter' +import { TokenOption, TokenSelectorFlow } from 'src/components/TokenSelector/types' +import { createEmptyBalanceOption } from 'src/components/TokenSelector/utils' +import { useTokenProjects } from 'src/features/dataApi/tokenProjects' +import { usePopularTokens } from 'src/features/dataApi/topTokens' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses' import { ChainId } from 'wallet/src/constants/chains' import { DAI, USDC, USDT, WBTC } from 'wallet/src/constants/tokens' @@ -11,21 +15,16 @@ import { usePortfolioBalances, useTokenBalancesGroupedByVisibility, } from 'wallet/src/features/dataApi/balances' -import { useTokenProjects } from 'wallet/src/features/dataApi/tokenProjects' -import { usePopularTokens } from 'wallet/src/features/dataApi/topTokens' import { CurrencyInfo, GqlResult, PortfolioBalance } from 'wallet/src/features/dataApi/types' import { usePersistedError } from 'wallet/src/features/dataApi/utils' import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' -import { useAppSelector } from 'wallet/src/state' -import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' -import { WalletEventName } from 'wallet/src/telemetry/constants' import { areAddressesEqual } from 'wallet/src/utils/addresses' import { buildNativeCurrencyId, buildWrappedNativeCurrencyId, currencyId, } from 'wallet/src/utils/currencyId' +import { flowToModalName } from './flowToModalName' // Use Mainnet base token addresses since TokenProjects query returns each token // on each network @@ -116,7 +115,7 @@ export function useFilterCallbacks( const onChangeChainFilter = useCallback( (newChainFilter: typeof chainFilter) => { setChainFilter(newChainFilter) - sendWalletAnalyticsEvent(WalletEventName.NetworkFilterSelected, { + sendMobileAnalyticsEvent(MobileEventName.NetworkFilterSelected, { chain: newChainFilter ?? 'All', modal: flowToModalName(flow), }) diff --git a/packages/wallet/src/components/TokenSelector/renderSuggestedTokenItem.tsx b/apps/mobile/src/components/TokenSelector/renderSuggestedTokenItem.tsx similarity index 82% rename from packages/wallet/src/components/TokenSelector/renderSuggestedTokenItem.tsx rename to apps/mobile/src/components/TokenSelector/renderSuggestedTokenItem.tsx index 5600f34920d..0f8f84fa164 100644 --- a/packages/wallet/src/components/TokenSelector/renderSuggestedTokenItem.tsx +++ b/apps/mobile/src/components/TokenSelector/renderSuggestedTokenItem.tsx @@ -1,10 +1,11 @@ -import { Flex } from 'ui/src' -import { SuggestedToken } from 'wallet/src/components/TokenSelector/SuggestedToken' +import React from 'react' +import { SuggestedToken } from 'src/components/TokenSelector/SuggestedToken' import { OnSelectCurrency, SuggestedTokenSection, TokenOption, -} from 'wallet/src/components/TokenSelector/types' +} from 'src/components/TokenSelector/types' +import { Flex } from 'ui/src' export function renderSuggestedTokenItem({ item: suggestedTokens, diff --git a/packages/wallet/src/components/TokenSelector/suggestedTokensKeyExtractor.tsx b/apps/mobile/src/components/TokenSelector/suggestedTokensKeyExtractor.tsx similarity index 70% rename from packages/wallet/src/components/TokenSelector/suggestedTokensKeyExtractor.tsx rename to apps/mobile/src/components/TokenSelector/suggestedTokensKeyExtractor.tsx index ed7a61d82b8..6874d7914ad 100644 --- a/packages/wallet/src/components/TokenSelector/suggestedTokensKeyExtractor.tsx +++ b/apps/mobile/src/components/TokenSelector/suggestedTokensKeyExtractor.tsx @@ -1,4 +1,4 @@ -import { TokenOption } from 'wallet/src/components/TokenSelector/types' +import { TokenOption } from 'src/components/TokenSelector/types' export function suggestedTokensKeyExtractor(suggestedTokens: TokenOption[]): string { return suggestedTokens.map((token) => token.currencyInfo.currencyId).join('-') diff --git a/packages/wallet/src/components/TokenSelector/types.ts b/apps/mobile/src/components/TokenSelector/types.ts similarity index 92% rename from packages/wallet/src/components/TokenSelector/types.ts rename to apps/mobile/src/components/TokenSelector/types.ts index 961f5b02519..740b0bee101 100644 --- a/packages/wallet/src/components/TokenSelector/types.ts +++ b/apps/mobile/src/components/TokenSelector/types.ts @@ -21,7 +21,11 @@ export type TokenSection = { export type SuggestedTokenSection = { title: string data: TokenOption[][] - rightElement?: JSX.Element } export type TokenSelectorListSections = Array + +export enum TokenSelectorFlow { + Swap, + Transfer, +} diff --git a/packages/wallet/src/components/TokenSelector/utils.ts b/apps/mobile/src/components/TokenSelector/utils.ts similarity index 96% rename from packages/wallet/src/components/TokenSelector/utils.ts rename to apps/mobile/src/components/TokenSelector/utils.ts index fde73601dc3..9fa7e5df601 100644 --- a/packages/wallet/src/components/TokenSelector/utils.ts +++ b/apps/mobile/src/components/TokenSelector/utils.ts @@ -1,5 +1,5 @@ +import { TokenOption, TokenSection } from 'src/components/TokenSelector/types' import { differenceWith } from 'utilities/src/primitives/array' -import { TokenOption, TokenSection } from 'wallet/src/components/TokenSelector/types' import { CurrencyInfo, PortfolioBalance } from 'wallet/src/features/dataApi/types' import { areCurrencyIdsEqual } from 'wallet/src/utils/currencyId' diff --git a/apps/mobile/src/components/Trace/Trace.tsx b/apps/mobile/src/components/Trace/Trace.tsx index 7d218a99631..87a32a33ad6 100644 --- a/apps/mobile/src/components/Trace/Trace.tsx +++ b/apps/mobile/src/components/Trace/Trace.tsx @@ -1,15 +1,20 @@ import { memo, PropsWithChildren } from 'react' -import { ManualPageViewScreen, MobileEventName } from 'src/features/telemetry/constants' +import { + ElementName, + ManualPageViewScreen, + MobileEventName, + ModalName, + SectionName, +} from 'src/features/telemetry/constants' import { AppScreen } from 'src/screens/Screens' -import { TraceProps, Trace as UntypedTrace } from 'utilities/src/telemetry/trace/Trace' -import { ElementNameType, ModalNameType, SectionNameType } from 'wallet/src/telemetry/constants' +import { Trace as UntypedTrace, TraceProps } from 'utilities/src/telemetry/trace/Trace' // Mobile specific version of ITraceContext interface MobileTraceContext { screen?: AppScreen | ManualPageViewScreen - section?: SectionNameType - modal?: ModalNameType - element?: ElementNameType + section?: SectionName + modal?: ModalName + element?: ElementName } interface MobileTracePropsOverrides { diff --git a/apps/mobile/src/components/Trace/TraceTabView.tsx b/apps/mobile/src/components/Trace/TraceTabView.tsx index eb10e591d20..81350cb0528 100644 --- a/apps/mobile/src/components/Trace/TraceTabView.tsx +++ b/apps/mobile/src/components/Trace/TraceTabView.tsx @@ -2,10 +2,10 @@ import { SharedEventName } from '@uniswap/analytics-events' import React from 'react' import { Route, TabView, TabViewProps } from 'react-native-tab-view' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { SectionName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' -import { SectionNameType } from 'wallet/src/telemetry/constants' -type TraceRouteProps = { key: SectionNameType } & Route +type TraceRouteProps = { key: SectionName } & Route export default function TraceTabView({ onIndexChange, diff --git a/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx b/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx index cf7dd03fb26..46c8a2877e5 100644 --- a/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx +++ b/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx @@ -6,8 +6,8 @@ import { TraceUserProperties } from 'src/components/Trace/TraceUserProperties' import * as biometricHooks from 'src/features/biometrics/hooks' import { AuthMethod, UserPropertyName } from 'src/features/telemetry/constants' import * as versionUtils from 'src/utils/version' -import * as useIsDarkModeFile from 'ui/src/hooks/useIsDarkMode' import { analytics } from 'utilities/src/telemetry/analytics/analytics' +import * as appearanceHooks from 'wallet/src/features/appearance/hooks' import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' import * as fiatCurrencyHooks from 'wallet/src/features/fiatCurrency/hooks' import * as languageHooks from 'wallet/src/features/language/hooks' @@ -78,7 +78,7 @@ describe('TraceUserProperties', () => { touchId: false, faceId: true, }) - mockFn(useIsDarkModeFile, 'useIsDarkMode', true) + mockFn(appearanceHooks, 'useIsDarkMode', true) mockFn(fiatCurrencyHooks, 'useAppFiatCurrency', FiatCurrency.UnitedStatesDollar) mockFn(languageHooks, 'useCurrentLanguageInfo', { loggingName: 'English' }) mockFn(appHooks, 'useAppSelector', { enabled: true }) @@ -140,7 +140,7 @@ describe('TraceUserProperties', () => { touchId: false, faceId: false, }) - mockFn(useIsDarkModeFile, 'useIsDarkMode', true) + mockFn(appearanceHooks, 'useIsDarkMode', true) mockFn(fiatCurrencyHooks, 'useAppFiatCurrency', FiatCurrency.UnitedStatesDollar) mockFn(languageHooks, 'useCurrentLanguageInfo', { loggingName: 'English' }) diff --git a/apps/mobile/src/components/Trace/TraceUserProperties.tsx b/apps/mobile/src/components/Trace/TraceUserProperties.tsx index 9e1ab45ea94..7709d21f769 100644 --- a/apps/mobile/src/components/Trace/TraceUserProperties.tsx +++ b/apps/mobile/src/components/Trace/TraceUserProperties.tsx @@ -6,11 +6,11 @@ import { useDeviceSupportsBiometricAuth, } from 'src/features/biometrics/hooks' import { setUserProperty } from 'src/features/telemetry' -import { UserPropertyName, getAuthMethod } from 'src/features/telemetry/constants' +import { getAuthMethod, UserPropertyName } from 'src/features/telemetry/constants' import { selectAllowAnalytics } from 'src/features/telemetry/selectors' import { getFullAppVersion } from 'src/utils/version' -import { useIsDarkMode } from 'ui/src' import { analytics } from 'utilities/src/telemetry/analytics/analytics' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { useAppFiatCurrency } from 'wallet/src/features/fiatCurrency/hooks' import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' import { BackupType } from 'wallet/src/features/wallet/accounts/types' diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx index 739d0ef0ccc..fdec03392ae 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx @@ -8,13 +8,14 @@ import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { DappConnectedNetworkModal } from 'src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal' import { DappConnectionItem } from 'src/components/WalletConnect/ConnectedDapps/DappConnectionItem' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { removePendingSession, WalletConnectSession, } from 'src/features/walletConnect/walletConnectSlice' -import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useDeviceDimensions } from 'ui/src' +import { AnimatedFlex, Flex, Text, TouchableArea, useDeviceDimensions } from 'ui/src' +import { Edit as EditIcon, Scan as ScanIcon } from 'ui/src/components/icons' import { spacing } from 'ui/src/theme' -import { ModalName } from 'wallet/src/telemetry/constants' type ConnectedDappsProps = { sessions: WalletConnectSession[] @@ -60,14 +61,14 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps setIsEditing(!isEditing) }}> {isEditing ? ( - + ) : ( - + )} ) : ( - + )} diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx index a00d3c63dc7..df06235c620 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx @@ -3,7 +3,9 @@ import React from 'react' import { useTranslation } from 'react-i18next' import 'react-native-reanimated' import { useAppDispatch } from 'src/app/hooks' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' +import { ModalName } from 'src/features/telemetry/constants' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { removeSession, WalletConnectSession } from 'src/features/walletConnect/walletConnectSlice' import { Button, Flex, Text } from 'ui/src' @@ -11,13 +13,11 @@ import { iconSizes } from 'ui/src/theme' import { logger } from 'utilities/src/logger/logger' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { CHAIN_INFO } from 'wallet/src/constants/chains' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { WalletConnectEvent } from 'wallet/src/features/walletConnect/types' -import { ModalName } from 'wallet/src/telemetry/constants' interface DappConnectedNetworkModalProps { session: WalletConnectSession onClose: () => void diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx index 8032b916ec9..c03f0693899 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx @@ -9,8 +9,9 @@ import { FadeIn, FadeOut } from 'react-native-reanimated' import { useAppDispatch } from 'src/app/hooks' import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' +import { ElementName } from 'src/features/telemetry/constants' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' -import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice' +import { removeSession, WalletConnectSession } from 'src/features/walletConnect/walletConnectSlice' import { disableOnPress } from 'src/utils/disableOnPress' import { AnimatedTouchableArea, Flex, Text, TouchableArea } from 'ui/src' import { iconSizes, spacing } from 'ui/src/theme' @@ -20,7 +21,6 @@ import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { WalletConnectEvent } from 'wallet/src/features/walletConnect/types' -import { ElementName } from 'wallet/src/telemetry/constants' export function DappConnectionItem({ session, diff --git a/apps/mobile/src/components/WalletConnect/NetworkLogos.tsx b/apps/mobile/src/components/WalletConnect/NetworkLogos.tsx index d814e4ab444..74706ef075a 100644 --- a/apps/mobile/src/components/WalletConnect/NetworkLogos.tsx +++ b/apps/mobile/src/components/WalletConnect/NetworkLogos.tsx @@ -3,7 +3,7 @@ import 'react-native-reanimated' import { Flex, FlexProps, Text } from 'ui/src' import { iconSizes, spacing } from 'ui/src/theme' import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' -import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' export type NetworkLogosProps = { chains: ChainId[] diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx index 056464b6cd3..25e38f3c385 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx @@ -5,7 +5,7 @@ import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util' import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice' import { Text } from 'ui/src' import { EthMethod } from 'wallet/src/features/walletConnect/types' -import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' +import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' export function HeaderText({ request, diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx index 68514111a57..0bc0b9934dc 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx @@ -10,6 +10,7 @@ import { SignRequest, WalletConnectRequest, } from 'src/features/walletConnect/walletConnectSlice' +import { ExplorerDataType, getExplorerLink } from 'src/utils/linking' import { useNoYoloParser } from 'src/utils/useNoYoloParser' import { Flex, Text, useSporeColors } from 'ui/src' import { iconSizes, TextVariantTokens } from 'ui/src/theme' @@ -19,7 +20,6 @@ import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { useENS } from 'wallet/src/features/ens/useENS' import { EthMethod, EthTransaction } from 'wallet/src/features/walletConnect/types' import { getValidAddress, shortenAddress } from 'wallet/src/utils/addresses' -import { ExplorerDataType, getExplorerLink } from 'wallet/src/utils/linking' const getStrMessage = (request: WalletConnectRequest): string => { if (request.type === EthMethod.PersonalSign || request.type === EthMethod.EthSign) { diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx index 6e23096c33a..b8b1a56eaab 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx @@ -9,7 +9,7 @@ import { useUSDValue } from 'wallet/src/features/gas/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { getSymbolDisplayText } from 'wallet/src/utils/currency' -import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' +import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' export function SpendingDetails({ value, diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx index 8b0195da3ba..c3afb9158d0 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx @@ -5,12 +5,18 @@ import React, { PropsWithChildren, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { StyleProp, ViewStyle } from 'react-native' import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { AccountDetails } from 'src/components/accounts/AccountDetails' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' +import { NetworkFee } from 'src/components/Network/NetworkFee' +import { NetworkPill } from 'src/components/Network/NetworkPill' import { ClientDetails, PermitInfo } from 'src/components/WalletConnect/RequestModal/ClientDetails' import { useHasSufficientFunds } from 'src/components/WalletConnect/RequestModal/hooks' import { RequestDetails } from 'src/components/WalletConnect/RequestModal/RequestDetails' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName } from 'src/features/telemetry/constants' +import { ElementName, MobileEventName, ModalName } from 'src/features/telemetry/constants' +import { NetworkFeeInfoModal } from 'src/features/transactions/swap/modals/NetworkFeeInfoModal' +import { BlockedAddressWarning } from 'src/features/trm/BlockedAddressWarning' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga' @@ -25,16 +31,10 @@ import { Button, Flex, Text, useSporeColors } from 'ui/src' import AlertTriangle from 'ui/src/assets/icons/alert-triangle.svg' import { iconSizes } from 'ui/src/theme' import { logger } from 'utilities/src/logger/logger' -import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' -import { NetworkFee } from 'wallet/src/components/network/NetworkFee' -import { NetworkPill } from 'wallet/src/components/network/NetworkPill' import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' import { GasSpeed } from 'wallet/src/features/gas/types' import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { NetworkFeeInfoModal } from 'wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal' -import { BlockedAddressWarning } from 'wallet/src/features/trm/BlockedAddressWarning' import { useIsBlocked, useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { @@ -43,7 +43,6 @@ import { WCEventType, WCRequestOutcome, } from 'wallet/src/features/walletConnect/types' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { areAddressesEqual } from 'wallet/src/utils/addresses' import { buildCurrencyId } from 'wallet/src/utils/currencyId' diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx index 80d46597a32..29e78173cc2 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx @@ -2,13 +2,15 @@ import { getSdkError } from '@walletconnect/utils' import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { AccountDetails } from 'src/components/accounts/AccountDetails' import { LinkButton } from 'src/components/buttons/LinkButton' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' import { PendingConnectionSwitchAccountModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal' import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName } from 'src/features/telemetry/constants' +import { ElementName, MobileEventName, ModalName } from 'src/features/telemetry/constants' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' import { getSessionNamespaces } from 'src/features/walletConnect/utils' @@ -21,8 +23,6 @@ import { import { AnimatedFlex, Button, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { ChainId } from 'wallet/src/constants/chains' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' @@ -37,7 +37,6 @@ import { WCEventType, WCRequestOutcome, } from 'wallet/src/features/walletConnect/types' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' type Props = { pendingSession: WalletConnectPendingSession diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx index a50c93622b4..5d506f58f52 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx @@ -1,11 +1,11 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { ActionSheetModal } from 'src/components/modals/ActionSheetModal' import { SwitchAccountOption } from 'src/components/WalletConnect/ScanSheet/SwitchAccountOption' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { Flex, Text } from 'ui/src' -import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal' import { Account } from 'wallet/src/features/wallet/accounts/types' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' type Props = { activeAccount: Account | null diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx index 409b8d3327d..53aa6c7f41e 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx @@ -1,12 +1,12 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { ActionSheetModal } from 'src/components/modals/ActionSheetModal' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { Flex, Separator, Text, useSporeColors } from 'ui/src' import Check from 'ui/src/assets/icons/check.svg' import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' -import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal' -import { ALL_SUPPORTED_CHAIN_IDS, CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' +import { ALL_SUPPORTED_CHAIN_IDS, ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' type Props = { selectedChainId: ChainId diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx index d32dcb18290..5f64b34b2ca 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { Flex, Separator, Text, Unicon, useSporeColors } from 'ui/src' +import { Unicon } from 'src/components/unicons/Unicon' +import { Flex, Separator, Text, useSporeColors } from 'ui/src' import Check from 'ui/src/assets/icons/check.svg' -import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { Account } from 'wallet/src/features/wallet/accounts/types' import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { shortenAddress } from 'wallet/src/utils/addresses' @@ -23,10 +23,13 @@ export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Elem - + + {displayName?.name} + {shortenAddress(account.address)} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx index b93598d10cc..86b1cab3d74 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx @@ -6,6 +6,7 @@ import 'react-native-reanimated' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks' import { BackButtonView } from 'src/components/layout/BackButtonView' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' import { WalletQRCode } from 'src/components/QRCodeScanner/WalletQRCode' @@ -14,25 +15,23 @@ import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ import { getSupportedURI, isAllowedUwULinkRequest, - parseScantasticParams, URIType, UWULINK_PREFIX, } from 'src/components/WalletConnect/ScanSheet/util' -import { openModal } from 'src/features/modals/modalSlice' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils' import { addRequest } from 'src/features/walletConnect/walletConnectSlice' -import { Flex, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' +import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import Scan from 'ui/src/assets/icons/receive.svg' import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import { iconSizes } from 'ui/src/theme' import { logger } from 'utilities/src/logger/logger' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { EthMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' type Props = { initialScreenState?: ScannerModalState @@ -53,8 +52,7 @@ export function WalletConnectModal({ const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false) const { preload, navigate } = useEagerExternalProfileRootNavigation() const dispatch = useAppDispatch() - const isUwULinkEnabled = useFeatureFlag(FEATURE_FLAGS.UwULink) - const isScantasticEnabled = useFeatureFlag(FEATURE_FLAGS.Scantastic) + const uwuLinkEnabled = useFeatureFlag(FEATURE_FLAGS.UwULink) // Update QR scanner states when pending session error alert is shown from WCv2 saga event channel useEffect(() => { @@ -72,12 +70,11 @@ export function WalletConnectModal({ } await selectionAsync() - const supportedURI = await getSupportedURI(uri, { isUwULinkEnabled, isScantasticEnabled }) + const supportedURI = await getSupportedURI(uri, uwuLinkEnabled) if (!supportedURI) { setShouldFreezeCamera(true) Alert.alert( t('Invalid QR Code'), - // TODO(EXT-495): Add Scantastic product name here when ready t( 'Make sure that you’re scanning a valid WalletConnect or Ethereum address QR code before trying again.' ), @@ -140,26 +137,6 @@ export function WalletConnectModal({ } } - if (supportedURI.type === URIType.Scantastic) { - const { pubKey, uuid, vendor, model, browser } = parseScantasticParams(supportedURI.value) - - setShouldFreezeCamera(true) - dispatch( - openModal({ - name: ModalName.Scantastic, - initialState: { - pubKey, - uuid, - vendor, - model, - browser, - }, - }) - ) - - return - } - if (supportedURI.type === URIType.UwULink) { setShouldFreezeCamera(true) try { @@ -225,8 +202,7 @@ export function WalletConnectModal({ setShouldFreezeCamera, shouldFreezeCamera, hasPendingSessionError, - isUwULinkEnabled, - isScantasticEnabled, + uwuLinkEnabled, t, dispatch, ] diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts b/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts index 94f314a3a14..90b3c31b86c 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts @@ -4,7 +4,7 @@ import { wcInUniwapScheme, wcUniversalLinkUrl, } from 'src/features/deepLinking/handleDeepLinkSaga.test' -import { CUSTOM_UNI_QR_CODE_PREFIX, URIType, getSupportedURI } from './util' +import { CUSTOM_UNI_QR_CODE_PREFIX, getSupportedURI, URIType } from './util' const VALID_WC_V1_URI = 'validWcV1Uri@1?relay-protocol=irn&symKey=51e' const VALID_WC_V2_URI = 'validWcV2Uri@2?relay-protocol=irn&symKey=51e' diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts b/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts index 5712e27dc5b..716684f58b5 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts @@ -5,7 +5,6 @@ import { UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, UNISWAP_WALLETCONNECT_URL, } from 'src/features/deepLinking/handleDeepLinkSaga' -import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { UwULinkRequest } from 'wallet/src/features/walletConnect/types' import { getValidAddress } from 'wallet/src/utils/addresses' @@ -14,7 +13,6 @@ export enum URIType { WalletConnectV2URL = 'walletconnect-v2', Address = 'address', EasterEgg = 'easter-egg', - Scantastic = 'scantastic', UwULink = 'uwu-link', } @@ -23,11 +21,6 @@ export type URIFormat = { value: string } -interface EnabledFeatureFlags { - isUwULinkEnabled: boolean - isScantasticEnabled: boolean -} - const UNISNAP_CONTRACT_ADDRESS = '0xFd2308677A0eb48e2d0c4038c12AA7DCb703e8DC' const UWULINK_CONTRACT_ALLOWLIST = [UNISNAP_CONTRACT_ADDRESS] const UWULINK_MAX_TXN_VALUE = '0.001' @@ -45,7 +38,7 @@ export function truncateDappName(name: string): string { export async function getSupportedURI( uri: string, - enabledFeatureFlags?: EnabledFeatureFlags + isUwULinkEnabled?: boolean ): Promise { if (!uri) { return undefined @@ -61,11 +54,6 @@ export async function getSupportedURI( return { type: URIType.Address, value: maybeMetamaskAddress } } - const maybeScantasticAddress = getScantasticAddress(uri) - if (enabledFeatureFlags?.isScantasticEnabled && maybeScantasticAddress) { - return { type: URIType.Scantastic, value: maybeScantasticAddress } - } - // The check for custom prefixes must be before the parseUri version 2 check because // parseUri(hello_uniwallet:[valid_wc_uri]) also returns version 2 const { uri: maybeCustomWcUri, type } = @@ -91,7 +79,7 @@ export async function getSupportedURI( return { type: URIType.EasterEgg, value: uri } } - if (enabledFeatureFlags?.isUwULinkEnabled && isUwULink(uri)) { + if (isUwULinkEnabled && isUwULink(uri)) { return { type: URIType.UwULink, value: uri.slice(UWULINK_PREFIX.length) } } } @@ -151,24 +139,3 @@ function getMetamaskAddress(uri: string): Nullable { return getValidAddress(uriParts[1], /*withChecksum=*/ true, /*log=*/ false) } - -// format is scantastic:// -function getScantasticAddress(uri: string): Nullable { - const uriParts = uri.split('://') - - if (uriParts.length < 2) { - return null - } - - return uriParts[1] || null -} - -/** parses scantastic params for a valid scantastic URI. */ -export function parseScantasticParams(uri: string): ScantasticModalState { - const pubKey = new URLSearchParams(uri).get('pubKey') || '' - const uuid = new URLSearchParams(uri).get('uuid') || '' - const vendor = new URLSearchParams(uri).get('vendor') || '' - const model = new URLSearchParams(uri).get('model') || '' - const browser = new URLSearchParams(uri).get('browser') || '' - return { pubKey, uuid, vendor, model, browser } -} diff --git a/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx b/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx index 7b41deda1ef..390156d095c 100644 --- a/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx +++ b/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx @@ -1,10 +1,14 @@ import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useAppDispatch } from 'src/app/hooks' +import { AccountDetails } from 'src/components/accounts/AccountDetails' +import { WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' import { WalletConnectRequestModal } from 'src/components/WalletConnect/RequestModal/WalletConnectRequestModal' import { PendingConnectionModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionModal' import { WalletConnectModal } from 'src/components/WalletConnect/ScanSheet/WalletConnectModal' import { closeModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' import { removePendingSession, @@ -16,15 +20,11 @@ import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' import { Flex, useSporeColors } from 'ui/src' import EyeIcon from 'ui/src/assets/icons/eye.svg' import { iconSizes } from 'ui/src/theme' -import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' import { useActiveAccount, useActiveAccountAddressWithThrow, useSignerAccounts, } from 'wallet/src/features/wallet/hooks' -import { ModalName } from 'wallet/src/telemetry/constants' import { areAddressesEqual } from 'wallet/src/utils/addresses' export function WalletConnectModals(): JSX.Element { diff --git a/apps/mobile/src/components/accounts/AccountCardItem.tsx b/apps/mobile/src/components/accounts/AccountCardItem.tsx index 6c07bc8dbe7..8de7df63a41 100644 --- a/apps/mobile/src/components/accounts/AccountCardItem.tsx +++ b/apps/mobile/src/components/accounts/AccountCardItem.tsx @@ -4,20 +4,19 @@ import { useTranslation } from 'react-i18next' import ContextMenu from 'react-native-context-menu-view' import { useAppDispatch } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' -import { NotificationBadge } from 'src/components/notifications/Badge' +import { useAccountList } from 'src/components/accounts/hooks' +import { AddressDisplay } from 'src/components/AddressDisplay' import { closeModal, openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' +import { setClipboard } from 'src/utils/clipboard' import { disableOnPress } from 'src/utils/disableOnPress' import { Flex, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { useAccountList } from 'wallet/src/features/accounts/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' -import { ModalName } from 'wallet/src/telemetry/constants' -import { setClipboard } from 'wallet/src/utils/clipboard' type AccountCardItemProps = { address: Address @@ -134,10 +133,10 @@ export function AccountCardItem({ @@ -152,11 +151,3 @@ export function AccountCardItem({ ) } - -const NotificationsBadgeContainer = ({ - children, - address, -}: { - children: React.ReactNode - address: string -}): JSX.Element => {children} diff --git a/apps/mobile/src/components/accounts/AccountDetails.test.tsx b/apps/mobile/src/components/accounts/AccountDetails.test.tsx new file mode 100644 index 00000000000..cca076953aa --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountDetails.test.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { AccountDetails } from 'src/components/accounts/AccountDetails' +import { render } from 'src/test/test-utils' +import { account } from 'wallet/src/test/fixtures' + +describe(AccountDetails, () => { + it('renders without error', () => { + const tree = render() + + expect(tree.toJSON()).toMatchSnapshot() + }) + + it('renders without error with chevron', () => { + const tree = render() + + expect(tree.toJSON()).toMatchSnapshot() + }) +}) diff --git a/packages/wallet/src/components/accounts/AccountDetails.tsx b/apps/mobile/src/components/accounts/AccountDetails.tsx similarity index 92% rename from packages/wallet/src/components/accounts/AccountDetails.tsx rename to apps/mobile/src/components/accounts/AccountDetails.tsx index b2f02cb3cfe..b9b896a81e5 100644 --- a/packages/wallet/src/components/accounts/AccountDetails.tsx +++ b/apps/mobile/src/components/accounts/AccountDetails.tsx @@ -1,5 +1,6 @@ +import React from 'react' +import { AddressDisplay } from 'src/components/AddressDisplay' import { Flex, Icons, Text } from 'ui/src' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { shortenAddress } from 'wallet/src/utils/addresses' export function AccountDetails({ diff --git a/apps/mobile/src/components/accounts/AccountHeader.tsx b/apps/mobile/src/components/accounts/AccountHeader.tsx index 8d1f76141b7..7b6d616aeb5 100644 --- a/apps/mobile/src/components/accounts/AccountHeader.tsx +++ b/apps/mobile/src/components/accounts/AccountHeader.tsx @@ -2,31 +2,30 @@ import { impactAsync, ImpactFeedbackStyle, selectionAsync } from 'expo-haptics' import React, { useCallback } from 'react' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' +import { AccountIcon } from 'src/components/AccountIcon' import { openModal } from 'src/features/modals/modalSlice' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' +import { setClipboard } from 'src/utils/clipboard' import { isDevBuild } from 'src/utils/version' import { Flex, Icons, Text, TouchableArea } from 'ui/src' -import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' -import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName' +import { useENSAvatar } from 'wallet/src/features/ens/api' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types' -import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' +import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { selectActiveAccount, selectActiveAccountAddress, } from 'wallet/src/features/wallet/selectors' -import { DisplayNameType } from 'wallet/src/features/wallet/types' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' -import { setClipboard } from 'wallet/src/utils/clipboard' export function AccountHeader(): JSX.Element { const activeAddress = useAppSelector(selectActiveAccountAddress) const account = useAppSelector(selectActiveAccount) const dispatch = useAppDispatch() - const { avatar } = useAvatar(activeAddress) + const { data: avatar } = useENSAvatar(activeAddress) const displayName = useDisplayName(activeAddress) const onPressAccountHeader = useCallback(() => { @@ -50,7 +49,7 @@ export function AccountHeader(): JSX.Element { } } - const walletHasName = displayName && displayName?.type !== DisplayNameType.Address + const walletHasName = displayName?.type !== 'address' const iconSize = 52 return ( @@ -91,7 +90,17 @@ export function AccountHeader(): JSX.Element { flexShrink={1} hitSlop={20} onPress={onPressAccountHeader}> - + + {displayName?.name} + + + + + + {sanitizeAddressText(shortenAddress(activeAddress))} + + + ) : ( diff --git a/apps/mobile/src/components/accounts/AccountList.tsx b/apps/mobile/src/components/accounts/AccountList.tsx index 8107b31e425..121d7b1aa5f 100644 --- a/apps/mobile/src/components/accounts/AccountList.tsx +++ b/apps/mobile/src/components/accounts/AccountList.tsx @@ -3,13 +3,13 @@ import { ComponentProps, default as React, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet } from 'react-native' import { AccountCardItem } from 'src/components/accounts/AccountCardItem' +import { useAccountList } from 'src/components/accounts/hooks' import { VirtualizedList } from 'src/components/layout/VirtualizedList' import { Flex, Text, useSporeColors } from 'ui/src' import { opacify, spacing } from 'ui/src/theme' import { useAsyncData } from 'utilities/src/react/hooks' import { PollingInterval } from 'wallet/src/constants/misc' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' -import { useAccountList } from 'wallet/src/features/accounts/hooks' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' // Most screens can fit more but this is set conservatively diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountDetails.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountDetails.test.tsx.snap new file mode 100644 index 00000000000..893df88180f --- /dev/null +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountDetails.test.tsx.snap @@ -0,0 +1,670 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountDetails renders without error 1`] = ` + + + + + + + + + + } + > + + + + + + + + + + + } + > + + + + + + + + + + + 0x​82D5...3Fa6 + + + + + + + + 0x82D5...3Fa6 + + + +`; + +exports[`AccountDetails renders without error with chevron 1`] = ` + + + + + + + + + + } + > + + + + + + + + + + + } + > + + + + + + + + + + + 0x​82D5...3Fa6 + + + + + + + + 0x82D5...3Fa6 + + + + + + + + + + +`; diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap index d84cbca8aed..8a900f7cfe6 100644 --- a/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap @@ -4,6 +4,7 @@ exports[`AccountHeader renders without error 1`] = ` + + Test Account + + + - - + - Test Account - - + - - - .cantswim.eth - - - - - - 0x​82D5...3Fa6 - - - - - - - - - - + strokeWidth="0.5" + /> + + diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap index 91f9348f9b9..99d295df45a 100644 --- a/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap @@ -4,6 +4,7 @@ exports[`AccountList renders without error 1`] = ` - - - 0x​82D5...3Fa6 - - + 0x​82D5...3Fa6 + diff --git a/packages/wallet/src/features/accounts/hooks.ts b/apps/mobile/src/components/accounts/hooks.ts similarity index 100% rename from packages/wallet/src/features/accounts/hooks.ts rename to apps/mobile/src/components/accounts/hooks.ts diff --git a/apps/mobile/src/components/animation/AnimateInOrder.tsx b/apps/mobile/src/components/animation/AnimateInOrder.tsx deleted file mode 100644 index c8118151678..00000000000 --- a/apps/mobile/src/components/animation/AnimateInOrder.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { impactAsync, ImpactFeedbackStyle } from 'expo-haptics' -import { PropsWithChildren, useEffect, useState } from 'react' -import { Flex, FlexProps } from 'ui/src' - -export const AnimateInOrder = ({ - children, - index, - animation = 'bouncy', - enterStyle = { o: 0, scale: 0.8 }, - exitStyle = { o: 0, scale: 0.8 }, - delayMs = 150, - hapticOnEnter, - ...rest -}: PropsWithChildren< - { - index: number - hapticOnEnter?: boolean - delayMs?: number - } & Pick & - FlexProps ->): JSX.Element => { - return ( - - - {children} - - - ) -} - -const Delay = ({ - children, - hapticOnEnter, - by, -}: PropsWithChildren<{ by: number; hapticOnEnter?: boolean }>): JSX.Element | null => { - const [done, setDone] = useState(false) - - useEffect(() => { - const showTimer = setTimeout(async () => { - if (hapticOnEnter) { - await impactAsync(ImpactFeedbackStyle.Light) - } - setDone(true) - }, by) - return () => clearTimeout(showTimer) - }, [by, hapticOnEnter]) - - return done ? <>{children} : null -} diff --git a/apps/mobile/src/components/buttons/CopyTextButton.tsx b/apps/mobile/src/components/buttons/CopyTextButton.tsx index 4f2aec39f93..0902a9c8ac2 100644 --- a/apps/mobile/src/components/buttons/CopyTextButton.tsx +++ b/apps/mobile/src/components/buttons/CopyTextButton.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { setClipboard } from 'src/utils/clipboard' import { Button, useSporeColors } from 'ui/src' import CheckCircle from 'ui/src/assets/icons/check-circle.svg' import CopySheets from 'ui/src/assets/icons/copy-sheets.svg' import { iconSizes } from 'ui/src/theme' import { useTimeout } from 'utilities/src/time/timing' -import { setClipboard } from 'wallet/src/utils/clipboard' interface Props { copyText?: string diff --git a/apps/mobile/src/components/buttons/FavoriteButton.tsx b/apps/mobile/src/components/buttons/FavoriteButton.tsx index 1b383c8559c..91a8327862c 100644 --- a/apps/mobile/src/components/buttons/FavoriteButton.tsx +++ b/apps/mobile/src/components/buttons/FavoriteButton.tsx @@ -8,8 +8,9 @@ import { useSharedValue, withTiming, } from 'react-native-reanimated' -import { AnimatedFlex, useIsDarkMode, useSporeColors } from 'ui/src' +import { AnimatedFlex, useSporeColors } from 'ui/src' import HeartIcon from 'ui/src/assets/icons/heart.svg' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' interface FavoriteButtonProps { isFavorited: boolean diff --git a/apps/mobile/src/components/buttons/LinkButton.tsx b/apps/mobile/src/components/buttons/LinkButton.tsx index 1ab18c7ec67..5758c5aa34c 100644 --- a/apps/mobile/src/components/buttons/LinkButton.tsx +++ b/apps/mobile/src/components/buttons/LinkButton.tsx @@ -1,8 +1,8 @@ import React, { useMemo } from 'react' +import { openUri } from 'src/utils/linking' import { Flex, FlexProps, Text, TouchableArea, TouchableAreaProps, useSporeColors } from 'ui/src' import ExternalLinkIcon from 'ui/src/assets/icons/external-link.svg' -import { TextVariantTokens, iconSizes } from 'ui/src/theme' -import { openUri } from 'wallet/src/utils/linking' +import { iconSizes, TextVariantTokens } from 'ui/src/theme' interface LinkButtonProps extends Omit { label: string diff --git a/packages/wallet/src/components/buttons/PasteButton.tsx b/apps/mobile/src/components/buttons/PasteButton.tsx similarity index 71% rename from packages/wallet/src/components/buttons/PasteButton.tsx rename to apps/mobile/src/components/buttons/PasteButton.tsx index e6c3f45d4b8..59f8630d102 100644 --- a/packages/wallet/src/components/buttons/PasteButton.tsx +++ b/apps/mobile/src/components/buttons/PasteButton.tsx @@ -1,6 +1,7 @@ +import React from 'react' import { useTranslation } from 'react-i18next' -import { Flex, Icons, Text, TouchableArea } from 'ui/src' -import { getClipboard } from 'wallet/src/utils/clipboard' +import { getClipboard } from 'src/utils/clipboard' +import { Button, Flex, Icons, Text, TouchableArea } from 'ui/src' export default function PasteButton({ inline, @@ -40,18 +41,13 @@ export default function PasteButton({ } return ( - } + size="small" + theme="tertiary" onPress={onPressButton} onPressIn={beforePress}> - - - {label} - - - - + {label} + ) } diff --git a/packages/wallet/src/components/buttons/PlusMinusButton.tsx b/apps/mobile/src/components/buttons/PlusMinusButton.tsx similarity index 97% rename from packages/wallet/src/components/buttons/PlusMinusButton.tsx rename to apps/mobile/src/components/buttons/PlusMinusButton.tsx index 060562243d8..080c97d6f76 100644 --- a/packages/wallet/src/components/buttons/PlusMinusButton.tsx +++ b/apps/mobile/src/components/buttons/PlusMinusButton.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { Flex, Icons, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' diff --git a/packages/wallet/src/components/buttons/Switch.tsx b/apps/mobile/src/components/buttons/Switch.tsx similarity index 97% rename from packages/wallet/src/components/buttons/Switch.tsx rename to apps/mobile/src/components/buttons/Switch.tsx index e9ddb35976b..14baa344c51 100644 --- a/packages/wallet/src/components/buttons/Switch.tsx +++ b/apps/mobile/src/components/buttons/Switch.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { Switch as BaseSwitch, SwitchProps, ViewProps } from 'react-native' import { Flex, useSporeColors } from 'ui/src' import { isAndroid } from 'wallet/src/utils/platform' diff --git a/packages/wallet/src/components/buttons/TransferArrowButton.tsx b/apps/mobile/src/components/buttons/TransferArrowButton.tsx similarity index 90% rename from packages/wallet/src/components/buttons/TransferArrowButton.tsx rename to apps/mobile/src/components/buttons/TransferArrowButton.tsx index 62fccd8b5bb..fe9b7301a9e 100644 --- a/packages/wallet/src/components/buttons/TransferArrowButton.tsx +++ b/apps/mobile/src/components/buttons/TransferArrowButton.tsx @@ -1,6 +1,6 @@ -import { ComponentProps } from 'react' +import React, { ComponentProps } from 'react' +import { Arrow } from 'src/components/icons/Arrow' import { Flex, TouchableArea, useSporeColors } from 'ui/src' -import { Arrow } from 'wallet/src/components/icons/Arrow' const ICON_SIZE = 20 diff --git a/apps/mobile/src/components/explore/ExploreSections.tsx b/apps/mobile/src/components/explore/ExploreSections.tsx index a1a014e92d6..21de5918169 100644 --- a/apps/mobile/src/components/explore/ExploreSections.tsx +++ b/apps/mobile/src/components/explore/ExploreSections.tsx @@ -9,6 +9,7 @@ import { FavoriteWalletsGrid } from 'src/components/explore/FavoriteWalletsGrid' import { SortButton } from 'src/components/explore/SortButton' import { TokenItem, TokenItemData } from 'src/components/explore/TokenItem' import { AnimatedBottomSheetFlatList } from 'src/components/layout/AnimatedFlatList' +import { Loader } from 'src/components/loading' import { AutoScrollProps } from 'src/components/sortableGrid' import { getClientTokensOrderByCompareFn, @@ -16,7 +17,7 @@ import { getTokensOrderByValues, } from 'src/features/explore/utils' import { usePollOnFocusOnly } from 'src/utils/hooks' -import { Flex, Loader, Text, useDeviceInsets } from 'ui/src' +import { Flex, Text, useDeviceInsets } from 'ui/src' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' import { ChainId } from 'wallet/src/constants/chains' diff --git a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx index c9149287594..91aa5d71bcd 100644 --- a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx +++ b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx @@ -1,8 +1,8 @@ import { default as React } from 'react' import { useTranslation } from 'react-i18next' +import { ElementName } from 'src/features/telemetry/constants' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { ElementName } from 'wallet/src/telemetry/constants' export function FavoriteHeaderRow({ title, diff --git a/apps/mobile/src/components/explore/FavoriteTokenCard.tsx b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx index 4fa2b05d4a6..dd3625ff456 100644 --- a/apps/mobile/src/components/explore/FavoriteTokenCard.tsx +++ b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx @@ -4,17 +4,19 @@ import { ViewProps } from 'react-native' import ContextMenu from 'react-native-context-menu-view' import { FadeIn, - SharedValue, + FadeOut, interpolate, + SharedValue, useAnimatedReaction, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' import { useAppDispatch } from 'src/app/hooks' -import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' -import RemoveButton from 'src/components/explore/RemoveButton' import { useExploreTokenContextMenu } from 'src/components/explore/hooks' +import RemoveButton from 'src/components/explore/RemoveButton' import { Loader } from 'src/components/loading' +import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import { SectionName } from 'src/features/telemetry/constants' import { disableOnPress } from 'src/utils/disableOnPress' import { usePollOnFocusOnly } from 'src/utils/hooks' import { AnimatedFlex, AnimatedTouchableArea, Flex, Text } from 'ui/src' @@ -25,13 +27,12 @@ import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' import { RelativeChange } from 'wallet/src/components/text/RelativeChange' import { ChainId } from 'wallet/src/constants/chains' import { PollingInterval } from 'wallet/src/constants/misc' -import { useFavoriteTokenCardQuery } from 'wallet/src/data/__generated__/types-and-hooks' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' +import { useFavoriteTokenCardQuery } from 'wallet/src/data/__generated__/types-and-hooks' import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { removeFavoriteToken } from 'wallet/src/features/favorites/slice' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { SectionName } from 'wallet/src/telemetry/constants' import { getSymbolDisplayText } from 'wallet/src/utils/currency' export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114 @@ -153,6 +154,7 @@ function FavoriteTokenCard({ bg="$surface2" borderRadius="$rounded16" entering={FadeIn} + exiting={FadeOut} hapticFeedback={!isEditing} hapticStyle={ImpactFeedbackStyle.Light} m="$spacing4" @@ -172,7 +174,11 @@ function FavoriteTokenCard({ /> {getSymbolDisplayText(token?.symbol)} - + {isEditing ? ( + + ) : ( + + )} diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx index e4f2fc1f4de..ed855388e01 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx @@ -5,16 +5,15 @@ import { ViewProps } from 'react-native' import ContextMenu from 'react-native-context-menu-view' import { useAppDispatch } from 'src/app/hooks' import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' +import { AccountIcon } from 'src/components/AccountIcon' import RemoveButton from 'src/components/explore/RemoveButton' import { disableOnPress } from 'src/utils/disableOnPress' -import { Flex, TouchableArea } from 'ui/src' -import { borderRadii, iconSizes } from 'ui/src/theme' -import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' -import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' +import { Flex, flexStyles, Text, TouchableArea } from 'ui/src' +import { borderRadii, iconSizes, imageSizes } from 'ui/src/theme' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { useENSAvatar } from 'wallet/src/features/ens/api' import { removeWatchedAddress } from 'wallet/src/features/favorites/slice' -import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' -import { DisplayNameType } from 'wallet/src/features/wallet/types' +import { useDisplayName } from 'wallet/src/features/wallet/hooks' type FavoriteWalletCardProps = { address: Address @@ -33,7 +32,7 @@ export default function FavoriteWalletCard({ const { preload, navigate } = useEagerExternalProfileNavigation() const displayName = useDisplayName(address) - const { avatar } = useAvatar(address) + const { data: avatar } = useENSAvatar(address) const icon = useMemo(() => { return @@ -84,15 +83,16 @@ export default function FavoriteWalletCard({ {icon} - + + {displayName?.name} + - + {isEditing ? : } diff --git a/apps/mobile/src/components/explore/RemoveButton.tsx b/apps/mobile/src/components/explore/RemoveButton.tsx index 5d0d24c30c4..a2cac94ee63 100644 --- a/apps/mobile/src/components/explore/RemoveButton.tsx +++ b/apps/mobile/src/components/explore/RemoveButton.tsx @@ -1,28 +1,22 @@ -import { useAnimatedStyle, withTiming } from 'react-native-reanimated' +import React from 'react' +import { FadeIn, FadeOut } from 'react-native-reanimated' import { AnimatedTouchableArea, Flex, TouchableAreaProps } from 'ui/src' import { imageSizes } from 'ui/src/theme' -type RemoveButtonProps = TouchableAreaProps & { - visible?: boolean -} - -export default function RemoveButton({ visible = true, ...rest }: RemoveButtonProps): JSX.Element { - const animatedVisibilityStyle = useAnimatedStyle(() => ({ - opacity: visible ? withTiming(1) : withTiming(0), - })) - +export default function RemoveButton(props: TouchableAreaProps): JSX.Element { return ( + zIndex="$tooltip"> ) diff --git a/apps/mobile/src/components/explore/SortButton.tsx b/apps/mobile/src/components/explore/SortButton.tsx index af7051f89fb..219da632992 100644 --- a/apps/mobile/src/components/explore/SortButton.tsx +++ b/apps/mobile/src/components/explore/SortButton.tsx @@ -9,10 +9,11 @@ import { import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { MobileEventName } from 'src/features/telemetry/constants' import { disableOnPress } from 'src/utils/disableOnPress' -import { Flex, Icons, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { logger } from 'utilities/src/logger/logger' import { TokenSortableField } from 'wallet/src/data/__generated__/types-and-hooks' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { setTokensOrderBy } from 'wallet/src/features/wallet/slice' import { ClientTokensOrderBy, TokensOrderBy } from 'wallet/src/features/wallet/types' interface FilterGroupProps { @@ -62,7 +63,7 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { const selectedMenuAction = menuActions[e.nativeEvent.index] // Handle switching selected sort option if (!selectedMenuAction) { - logger.error(new Error('Unexpected context menu index selected'), { + logger.error('Unexpected context menu index selected', { tags: { file: 'SortButton', function: 'SortButtonContextMenu:onPress' }, }) return diff --git a/apps/mobile/src/components/explore/TokenItem.tsx b/apps/mobile/src/components/explore/TokenItem.tsx index 88bb2a60e1a..98138665158 100644 --- a/apps/mobile/src/components/explore/TokenItem.tsx +++ b/apps/mobile/src/components/explore/TokenItem.tsx @@ -6,7 +6,7 @@ import { useExploreTokenContextMenu } from 'src/components/explore/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { TokenMetadata } from 'src/components/tokens/TokenMetadata' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName } from 'src/features/telemetry/constants' +import { MobileEventName, SectionName } from 'src/features/telemetry/constants' import { disableOnPress } from 'src/utils/disableOnPress' import { AnimatedFlex, Flex, Text, TouchableArea } from 'ui/src' import { NumberType } from 'utilities/src/format/types' @@ -15,7 +15,6 @@ import { RelativeChange } from 'wallet/src/components/text/RelativeChange' import { ChainId } from 'wallet/src/constants/chains' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types' -import { SectionName } from 'wallet/src/telemetry/constants' import { buildCurrencyId, buildNativeCurrencyId, diff --git a/apps/mobile/src/components/explore/hooks.test.ts b/apps/mobile/src/components/explore/hooks.test.ts index 188b43e66dc..4d1eccea01a 100644 --- a/apps/mobile/src/components/explore/hooks.test.ts +++ b/apps/mobile/src/components/explore/hooks.test.ts @@ -3,11 +3,11 @@ import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-c import { act } from 'react-test-renderer' import configureMockStore from 'redux-mock-store' import { useExploreTokenContextMenu } from 'src/components/explore/hooks' +import { SectionName } from 'src/features/telemetry/constants' import { renderHookWithProviders } from 'src/test/render' import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks' import { FavoritesState } from 'wallet/src/features/favorites/slice' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { SectionName } from 'wallet/src/telemetry/constants' import { DaiAsset } from 'wallet/src/test/gqlFixtures' const tokenId = DaiAsset.address?.toLowerCase() ?? '' @@ -49,7 +49,7 @@ describe(useExploreTokenContextMenu, () => { onPress: expect.any(Function), }), expect.objectContaining({ - title: 'Receive', + title: 'Copy contract address', onPress: expect.any(Function), }), expect.objectContaining({ @@ -84,7 +84,7 @@ describe(useExploreTokenContextMenu, () => { onPress: expect.any(Function), }), expect.objectContaining({ - title: 'Receive', + title: 'Copy contract address', onPress: expect.any(Function), }), ]) @@ -138,7 +138,7 @@ describe(useExploreTokenContextMenu, () => { onPress: expect.any(Function), }), expect.objectContaining({ - title: 'Receive', + title: 'Copy contract address', onPress: expect.any(Function), }), expect.objectContaining({ @@ -149,7 +149,7 @@ describe(useExploreTokenContextMenu, () => { }) it("dispatches add to favorites redux action when 'Favorite token' is pressed", async () => { - const store = mockStore({ favorites: { tokens: [] }, appearance: { theme: 'system' } }) + const store = mockStore({ favorites: { tokens: [] } }) const { result } = renderHookWithProviders( () => useExploreTokenContextMenu(tokenMenuParams), { resolvers, store } @@ -178,7 +178,6 @@ describe(useExploreTokenContextMenu, () => { it("dispatches remove from favorites redux action when 'Remove favorite' is pressed", async () => { const store = mockStore({ favorites: { tokens: [tokenMenuParams.currencyId.toLowerCase()] }, - appearance: { theme: 'system' }, }) const { result } = renderHookWithProviders( () => useExploreTokenContextMenu(tokenMenuParams), @@ -207,10 +206,7 @@ describe(useExploreTokenContextMenu, () => { }) it('dispatches swap redux action when swap is pressed', async () => { - const store = mockStore({ - favorites: { tokens: [] }, - selectedAppearanceSettings: { theme: 'system' }, - }) + const store = mockStore({ favorites: { tokens: [] } }) const { result } = renderHookWithProviders(() => useExploreTokenContextMenu(tokenMenuParams), { store, resolvers, diff --git a/apps/mobile/src/components/explore/hooks.ts b/apps/mobile/src/components/explore/hooks.ts index e3beb0276c7..817da05f623 100644 --- a/apps/mobile/src/components/explore/hooks.ts +++ b/apps/mobile/src/components/explore/hooks.ts @@ -3,11 +3,18 @@ import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { NativeSyntheticEvent, Share } from 'react-native' import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' -import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks' import { openModal } from 'src/features/modals/modalSlice' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { + ElementName, + MobileEventName, + ModalName, + SectionName, + ShareableEntity, +} from 'src/features/telemetry/constants' +import { useCopyTokenAddressCallback } from 'src/features/tokens/hooks' +import { getTokenUrl } from 'src/utils/linking' import { logger } from 'utilities/src/logger/logger' import { ChainId } from 'wallet/src/constants/chains' import { AssetType } from 'wallet/src/entities/assets' @@ -16,14 +23,13 @@ import { TransactionState, } from 'wallet/src/features/transactions/transactionState/types' import { useAppDispatch } from 'wallet/src/state' -import { ElementName, ModalName, SectionNameType } from 'wallet/src/telemetry/constants' + import { CurrencyId, currencyIdToAddress } from 'wallet/src/utils/currencyId' -import { getTokenUrl } from 'wallet/src/utils/linking' interface TokenMenuParams { currencyId: CurrencyId chainId: ChainId - analyticsSection: SectionNameType + analyticsSection: SectionName // token, which are in favorite section would have it defined onEditFavorites?: () => void } @@ -46,13 +52,7 @@ export function useExploreTokenContextMenu({ // currencyId, where we have hardcoded addresses for native currencies const currencyAddress = currencyIdToAddress(currencyId) - const onPressReceive = useCallback( - () => - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) - ), - [dispatch] - ) + const onPressCopyContractAddress = useCopyTokenAddressCallback(currencyAddress) const onPressShare = useCallback(async () => { const tokenUrl = getTokenUrl(currencyId) @@ -114,9 +114,9 @@ export function useExploreTokenContextMenu({ : []), { title: t('Swap'), systemIcon: 'arrow.2.squarepath', onPress: onPressSwap }, { - title: t('Receive'), - systemIcon: 'qrcode', - onPress: onPressReceive, + title: t('Copy contract address'), + systemIcon: 'doc.on.doc', + onPress: onPressCopyContractAddress, }, ...(!onEditFavorites ? [ @@ -134,7 +134,7 @@ export function useExploreTokenContextMenu({ onPressToggleFavorite, onEditFavorites, onPressSwap, - onPressReceive, + onPressCopyContractAddress, onPressShare, ] ) diff --git a/packages/wallet/src/features/search/SearchContext.tsx b/apps/mobile/src/components/explore/search/SearchContext.tsx similarity index 100% rename from packages/wallet/src/features/search/SearchContext.tsx rename to apps/mobile/src/components/explore/search/SearchContext.tsx diff --git a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx index 2c6ad9a3a5a..14e4e045d8b 100644 --- a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx +++ b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' @@ -7,28 +7,22 @@ import { SearchPopularNFTCollections } from 'src/components/explore/search/Searc import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens' import { renderSearchItem } from 'src/components/explore/search/SearchResultsSection' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' +import { clearSearchHistory } from 'src/features/explore/searchHistorySlice' +import { SearchResultType, WalletSearchResult } from 'src/features/explore/SearchResult' +import { selectSearchHistory } from 'src/features/explore/selectSearchHistory' import { AnimatedFlex, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import ClockIcon from 'ui/src/assets/icons/clock.svg' import TrendArrowIcon from 'ui/src/assets/icons/trend-up.svg' import { iconSizes } from 'ui/src/theme' -import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' -import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' -import { clearSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { - SearchResult, - SearchResultType, - WalletSearchResult, -} from 'wallet/src/features/search/SearchResult' -import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory' export const SUGGESTED_WALLETS: WalletSearchResult[] = [ { - type: SearchResultType.ENSAddress, + type: SearchResultType.Wallet, address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', ensName: 'vitalik.eth', }, { - type: SearchResultType.ENSAddress, + type: SearchResultType.Wallet, address: '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3', ensName: 'hayden.eth', }, @@ -38,28 +32,11 @@ export function SearchEmptySection(): JSX.Element { const { t } = useTranslation() const dispatch = useAppDispatch() const searchHistory = useAppSelector(selectSearchHistory) - const unitagFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) const onPressClearSearchHistory = (): void => { dispatch(clearSearchHistory()) } - const modifiedHistory: SearchResult[] = useMemo( - () => - searchHistory.map((historyItem: SearchResult) => { - if (!unitagFeatureFlagEnabled && historyItem.type === SearchResultType.Unitag) { - return { - type: SearchResultType.ENSAddress, - address: historyItem.address, - searchId: historyItem.searchId, - } - } else { - return historyItem - } - }), - [searchHistory, unitagFeatureFlagEnabled] - ) - // Show search history (if applicable), trending tokens, and wallets return ( @@ -81,7 +58,7 @@ export function SearchEmptySection(): JSX.Element { } - data={modifiedHistory} + data={searchHistory} renderItem={(props): JSX.Element | null => renderSearchItem({ ...props, searchContext: { isHistory: true } }) } diff --git a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx index 7a310c03f7b..11642c787ad 100644 --- a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx +++ b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx @@ -5,12 +5,10 @@ import { getSearchResultId, gqlNFTToNFTCollectionSearchResult, } from 'src/components/explore/search/utils' -import { Inset, Loader } from 'ui/src' +import { Loader } from 'src/components/loading' +import { NFTCollectionSearchResult, SearchResultType } from 'src/features/explore/SearchResult' +import { Inset } from 'ui/src' import { useSearchPopularNftCollectionsQuery } from 'wallet/src/data/__generated__/types-and-hooks' -import { - NFTCollectionSearchResult, - SearchResultType, -} from 'wallet/src/features/search/SearchResult' function isNFTCollectionSearchResult( result: NFTCollectionSearchResult | null diff --git a/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx b/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx index 12ba3db5494..e2f1c26d602 100644 --- a/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx +++ b/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx @@ -7,7 +7,7 @@ import { EthToken, TopTokens } from 'wallet/src/test/gqlFixtures' const resolvers: Resolvers = { Query: { topTokens: () => TopTokens, - tokens: () => [{ ...EthToken, address: null }], + tokens: () => [EthToken], }, } diff --git a/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx b/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx index fb83fb2bea8..872f7c4162f 100644 --- a/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx +++ b/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx @@ -2,10 +2,11 @@ import React, { useMemo } from 'react' import { FlatList, ListRenderItemInfo } from 'react-native' import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' import { getSearchResultId } from 'src/components/explore/search/utils' -import { Inset, Loader } from 'ui/src' +import { Loader } from 'src/components/loading' +import { SearchResultType, TokenSearchResult } from 'src/features/explore/SearchResult' +import { TopToken, usePopularTokens } from 'src/features/tokens/hooks' +import { Inset } from 'ui/src' import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' -import { TopToken, usePopularTokens } from 'wallet/src/features/tokens/hooks' function gqlTokenToTokenSearchResult(token: Maybe): TokenSearchResult | null { if (!token || !token.project) { diff --git a/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx index 4164b79d84e..4664821d13d 100644 --- a/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx +++ b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx @@ -2,7 +2,8 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { FadeIn, FadeOut } from 'react-native-reanimated' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' -import { AnimatedFlex, Flex, Loader } from 'ui/src' +import { Loader } from 'src/components/loading' +import { AnimatedFlex, Flex } from 'ui/src' export const SearchResultsLoader = (): JSX.Element => { const { t } = useTranslation() diff --git a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx index dc91e2619c6..0b7aa369c4d 100644 --- a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx +++ b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx @@ -2,36 +2,34 @@ import React, { useCallback, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import { FlatList, ListRenderItemInfo } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader' -import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' -import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem' import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem' import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem' import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' -import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnitagItem' +import { SearchWalletItem } from 'src/components/explore/search/items/SearchWalletItem' +import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader' +import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { formatNFTCollectionSearchResults, formatTokenSearchResults, getSearchResultId, } from 'src/components/explore/search/utils' -import { AnimatedFlex, Flex, Text } from 'ui/src' -import { logger } from 'utilities/src/logger/logger' -import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' -import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' -import { SafetyLevel, useExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks' -import { useENS } from 'wallet/src/features/ens/useENS' -import { SearchContext } from 'wallet/src/features/search/SearchContext' import { NFTCollectionSearchResult, SearchResultType, TokenSearchResult, WalletSearchResult, -} from 'wallet/src/features/search/SearchResult' -import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress' -import { useUnitagByAddress, useUnitagByName } from 'wallet/src/features/unitags/hooks' +} from 'src/features/explore/SearchResult' +import { useIsSmartContractAddress } from 'src/features/transactions/transfer/hooks' +import { AnimatedFlex, Flex, Text } from 'ui/src' +import { logger } from 'utilities/src/logger/logger' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' +import { SafetyLevel, useExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks' +import { useENS } from 'wallet/src/features/ens/useENS' import i18n from 'wallet/src/i18n/i18n' import { getValidAddress } from 'wallet/src/utils/addresses' import { SEARCH_RESULT_HEADER_KEY } from './constants' +import { SearchContext } from './SearchContext' import { SearchResultOrHeader } from './types' const WalletHeaderItem: SearchResultOrHeader = { @@ -89,23 +87,15 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): loading: ensLoading, } = useENS(ChainId.Mainnet, searchQuery, true) - // Search for matching Unitag by name - const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(searchQuery) - - const validAddress: Address | undefined = getValidAddress(searchQuery, true, false) + const validAddress: Address | null = getValidAddress(searchQuery, true, false) ? searchQuery - : undefined - - // Search for matching Unitag by address - const { unitag: unitagByAddress, loading: unitagByAddressLoading } = - useUnitagByAddress(validAddress) + : null // Search for matching EOA wallet address const { isSmartContractAddress, loading: loadingIsSmartContractAddress } = - useIsSmartContractAddress(validAddress, ChainId.Mainnet) + useIsSmartContractAddress(validAddress ?? undefined, ChainId.Mainnet) - const walletsLoading = - ensLoading || loadingIsSmartContractAddress || unitagLoading || unitagByAddressLoading + const walletsLoading = ensLoading || loadingIsSmartContractAddress const onRetry = useCallback(async () => { await refetch() @@ -114,48 +104,30 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): const hasENSResult = ensName && ensAddress const hasEOAResult = validAddress && !isSmartContractAddress const walletSearchResults: WalletSearchResult[] = useMemo(() => { - const results: WalletSearchResult[] = [] - - if (unitagByName?.address?.address && unitagByName?.username) { - results.push({ - type: SearchResultType.Unitag, - address: unitagByName.address.address, - unitag: unitagByName.username, - }) + if (hasENSResult) { + return [ + { + type: SearchResultType.Wallet, + address: ensAddress, + ensName, + }, + ] } - - // Do not show ENS result if it is the same as the Unitag result - if (hasENSResult && ensAddress !== unitagByName?.address?.address) { - results.push({ - type: SearchResultType.ENSAddress, - address: ensAddress, - ensName, - }) + if (hasEOAResult) { + return [ + { + type: SearchResultType.Wallet, + address: validAddress, + }, + ] } - - if (unitagByAddress?.username && validAddress) { - results.push({ - type: SearchResultType.Unitag, - address: validAddress, - unitag: unitagByAddress.username, - }) - } - - // Do not show EOA address result if there is a Unitag result by address - if (hasEOAResult && !unitagByAddress) { - results.push({ - type: SearchResultType.ENSAddress, - address: validAddress, - }) - } - - return results as WalletSearchResult[] - }, [ensAddress, ensName, unitagByName, unitagByAddress, hasENSResult, hasEOAResult, validAddress]) + return [] + }, [ensAddress, ensName, hasENSResult, hasEOAResult, validAddress]) const countTokenResults = tokenResults?.length ?? 0 const countNftCollectionResults = nftCollectionResults?.length ?? 0 - const countWalletResults = walletSearchResults.length - const countTotalResults = countTokenResults + countNftCollectionResults + countWalletResults + const countENSResults = hasENSResult || hasEOAResult ? 1 : 0 + const countTotalResults = countTokenResults + countNftCollectionResults + countENSResults // Only consider queries with the .eth suffix as an exact ENS match const exactENSMatch = @@ -174,8 +146,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified)) - const showWalletSectionFirst = - unitagByName || unitagByAddress || (exactENSMatch && !prefixTokenMatch) + const showWalletSectionFirst = exactENSMatch && !prefixTokenMatch const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => { @@ -274,7 +245,6 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): } // Render function for FlatList of SearchResult items - export const renderSearchItem = ({ item: searchResult, searchContext, @@ -289,10 +259,8 @@ export const renderSearchItem = ({ ) case SearchResultType.Token: return - case SearchResultType.ENSAddress: - return - case SearchResultType.Unitag: - return + case SearchResultType.Wallet: + return case SearchResultType.NFTCollection: return case SearchResultType.Etherscan: diff --git a/apps/mobile/src/components/explore/search/__snapshots__/SearchPopularTokens.test.tsx.snap b/apps/mobile/src/components/explore/search/__snapshots__/SearchPopularTokens.test.tsx.snap index 18278525205..55dd8b3cc1c 100644 --- a/apps/mobile/src/components/explore/search/__snapshots__/SearchPopularTokens.test.tsx.snap +++ b/apps/mobile/src/components/explore/search/__snapshots__/SearchPopularTokens.test.tsx.snap @@ -4,6 +4,7 @@ exports[`SearchPopularTokens renders without error 1`] = ` - - - - - {completedENSName || formattedAddress} - - {showOwnedBy ? ( - - {t('Owned by {{owner}}', { - owner: primaryENSName || formattedAddress, - })} - - ) : null} - - - - ) -} diff --git a/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx index 420e0b792cf..b00f0c53369 100644 --- a/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx @@ -1,16 +1,16 @@ import { ImpactFeedbackStyle } from 'expo-haptics' import { default as React } from 'react' import { useAppDispatch } from 'src/app/hooks' +import { Arrow } from 'src/components/icons/Arrow' import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' +import { addToSearchHistory } from 'src/features/explore/searchHistorySlice' +import { EtherscanSearchResult } from 'src/features/explore/SearchResult' +import { ElementName } from 'src/features/telemetry/constants' +import { ExplorerDataType, getExplorerLink, openUri } from 'src/utils/linking' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { Arrow } from 'wallet/src/components/icons/Arrow' import { ChainId } from 'wallet/src/constants/chains' -import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { EtherscanSearchResult } from 'wallet/src/features/search/SearchResult' -import { ElementName } from 'wallet/src/telemetry/constants' import { shortenAddress } from 'wallet/src/utils/addresses' -import { ExplorerDataType, getExplorerLink, openUri } from 'wallet/src/utils/linking' type SearchEtherscanItemProps = { etherscanResult: EtherscanSearchResult diff --git a/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx index a4dc732159a..a7ce5512dce 100644 --- a/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx @@ -2,19 +2,15 @@ import { ImpactFeedbackStyle } from 'expo-haptics' import { default as React } from 'react' import { useAppDispatch } from 'src/app/hooks' import { useAppStackNavigation } from 'src/app/navigation/types' +import { SearchContext } from 'src/components/explore/search/SearchContext' +import { addToSearchHistory } from 'src/features/explore/searchHistorySlice' +import { NFTCollectionSearchResult, SearchResultType } from 'src/features/explore/SearchResult' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName } from 'src/features/telemetry/constants' +import { ElementName, MobileEventName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { - NFTCollectionSearchResult, - SearchResultType, -} from 'wallet/src/features/search/SearchResult' -import { ElementName } from 'wallet/src/telemetry/constants' type NFTCollectionItemProps = { collection: NFTCollectionSearchResult diff --git a/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx b/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx index bf66ce706c6..1d704c0fe7f 100644 --- a/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx @@ -3,19 +3,19 @@ import { default as React } from 'react' import ContextMenu from 'react-native-context-menu-view' import { useAppDispatch } from 'src/app/hooks' import { useExploreTokenContextMenu } from 'src/components/explore/hooks' +import { SearchContext } from 'src/components/explore/search/SearchContext' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import WarningIcon from 'src/components/tokens/WarningIcon' +import { addToSearchHistory } from 'src/features/explore/searchHistorySlice' +import { SearchResultType, TokenSearchResult } from 'src/features/explore/SearchResult' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName } from 'src/features/telemetry/constants' +import { ElementName, MobileEventName, SectionName } from 'src/features/telemetry/constants' import { disableOnPress } from 'src/utils/disableOnPress' -import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { Flex, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' -import WarningIcon from 'wallet/src/components/icons/WarningIcon' import { SafetyLevel } from 'wallet/src/data/__generated__/types-and-hooks' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' -import { ElementName, SectionName } from 'wallet/src/telemetry/constants' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { shortenAddress } from 'wallet/src/utils/addresses' import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' diff --git a/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx b/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx deleted file mode 100644 index bcfc2baab2e..00000000000 --- a/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' -import { Flex } from 'ui/src' -import { imageSizes } from 'ui/src/theme' -import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' -import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { UnitagSearchResult } from 'wallet/src/features/search/SearchResult' -import { useAvatar } from 'wallet/src/features/wallet/hooks' -import { DisplayNameType } from 'wallet/src/features/wallet/types' - -type SearchUnitagItemProps = { - searchResult: UnitagSearchResult - searchContext?: SearchContext -} - -export function SearchUnitagItem({ - searchResult, - searchContext, -}: SearchUnitagItemProps): JSX.Element { - const { address, unitag } = searchResult - const { avatar } = useAvatar(address) - - const displayName = { name: unitag, type: DisplayNameType.Unitag } - - return ( - - - - - - - ) -} diff --git a/apps/mobile/src/components/explore/search/items/SearchWalletItem.tsx b/apps/mobile/src/components/explore/search/items/SearchWalletItem.tsx new file mode 100644 index 00000000000..7e2a9fd67ce --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchWalletItem.tsx @@ -0,0 +1,117 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import ContextMenu from 'react-native-context-menu-view' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' +import { AccountIcon } from 'src/components/AccountIcon' +import { SearchContext } from 'src/components/explore/search/SearchContext' +import { addToSearchHistory } from 'src/features/explore/searchHistorySlice' +import { WalletSearchResult } from 'src/features/explore/SearchResult' +import { useToggleWatchedWalletCallback } from 'src/features/favorites/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { disableOnPress } from 'src/utils/disableOnPress' +import { Flex, Text, TouchableArea } from 'ui/src' +import { imageSizes } from 'ui/src/theme' +import { useENSAvatar, useENSName } from 'wallet/src/features/ens/api' +import { getCompletedENSName } from 'wallet/src/features/ens/useENS' +import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' + +type SearchWalletItemProps = { + wallet: WalletSearchResult + searchContext?: SearchContext +} + +export function SearchWalletItem({ wallet, searchContext }: SearchWalletItemProps): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { preload, navigate } = useEagerExternalProfileNavigation() + + // Use `savedPrimaryEnsName` for WalletSearchResults that are stored in the search history + // so that we don't have to do an additional ENS fetch when loading search history + const { address, ensName, primaryENSName: savedPrimaryENSName } = wallet + const formattedAddress = sanitizeAddressText(shortenAddress(address)) + + /* + * Fetch primary ENS associated with `address` since it may resolve to an + * ENS different than the `ensName` searched + * ex. if searching `uni.eth` resolves to 0x123, and the primary ENS for 0x123 + * is `uniswap.eth`, then we should show "uni.eth | owned by uniswap.eth" + */ + const completedENSName = getCompletedENSName(ensName ?? null) + const { data: fetchedPrimaryENSName, loading: isFetchingPrimaryENSName } = useENSName( + savedPrimaryENSName ? undefined : address + ) + + const primaryENSName = savedPrimaryENSName ?? fetchedPrimaryENSName + const isPrimaryENSName = completedENSName === primaryENSName + const showOwnedBy = !isFetchingPrimaryENSName && !isPrimaryENSName + + const { data: avatar } = useENSAvatar(address) + + const isFavorited = useAppSelector(selectWatchedAddressSet).has(address) + + const onPress = (): void => { + navigate(address) + if (searchContext) { + sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, { + query: searchContext.query, + name: ensName ?? address, + address, + type: 'address', + suggestion_count: searchContext.suggestionCount, + position: searchContext.position, + isHistory: searchContext.isHistory, + }) + } + dispatch( + addToSearchHistory({ + searchResult: { ...wallet, primaryENSName: primaryENSName ?? undefined }, + }) + ) + } + + const toggleFavoriteWallet = useToggleWatchedWalletCallback(address) + + const menuActions = useMemo(() => { + return isFavorited + ? [{ title: t('Remove favorite'), systemIcon: 'heart.fill' }] + : [{ title: t('Favorite wallet'), systemIcon: 'heart' }] + }, [isFavorited, t]) + + return ( + + => { + await preload(address) + }}> + + + + + {completedENSName || formattedAddress} + + {showOwnedBy ? ( + + {t('Owned by {{owner}}', { + owner: primaryENSName || formattedAddress, + })} + + ) : null} + + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx b/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx deleted file mode 100644 index 07f7ecf76fe..00000000000 --- a/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { ImpactFeedbackStyle } from 'expo-haptics' -import React, { PropsWithChildren, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import ContextMenu from 'react-native-context-menu-view' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' -import { useToggleWatchedWalletCallback } from 'src/features/favorites/hooks' -import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName } from 'src/features/telemetry/constants' -import { disableOnPress } from 'src/utils/disableOnPress' -import { TouchableArea } from 'ui/src' -import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { SearchResultType, WalletSearchResult } from 'wallet/src/features/search/SearchResult' - -type SearchWalletItemBaseProps = { - searchResult: WalletSearchResult - searchContext?: SearchContext -} - -export function SearchWalletItemBase({ - children, - searchResult, - searchContext, -}: PropsWithChildren): JSX.Element { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const { preload, navigate } = useEagerExternalProfileNavigation() - const { address, type } = searchResult - const isFavorited = useAppSelector(selectWatchedAddressSet).has(address) - - const onPress = (): void => { - navigate(address) - if (searchContext) { - sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, { - query: searchContext.query, - name: - type === SearchResultType.Unitag ? searchResult.unitag : searchResult.ensName ?? address, - address, - type: 'address', - suggestion_count: searchContext.suggestionCount, - position: searchContext.position, - isHistory: searchContext.isHistory, - }) - } - - if (type === SearchResultType.Unitag) { - dispatch( - addToSearchHistory({ - searchResult, - }) - ) - } else { - dispatch( - addToSearchHistory({ - searchResult: { - ...searchResult, - primaryENSName: searchResult.primaryENSName ?? undefined, - }, - }) - ) - } - } - - const toggleFavoriteWallet = useToggleWatchedWalletCallback(address) - - const menuActions = useMemo(() => { - return isFavorited - ? [{ title: t('Remove favorite'), systemIcon: 'heart.fill' }] - : [{ title: t('Favorite wallet'), systemIcon: 'heart' }] - }, [isFavorited, t]) - - return ( - - => { - await preload(address) - }}> - {children} - - - ) -} diff --git a/apps/mobile/src/components/explore/search/types.tsx b/apps/mobile/src/components/explore/search/types.tsx index 464e2749aa1..b4e5b97599a 100644 --- a/apps/mobile/src/components/explore/search/types.tsx +++ b/apps/mobile/src/components/explore/search/types.tsx @@ -1,4 +1,4 @@ -import { SearchResult } from 'wallet/src/features/search/SearchResult' +import { SearchResult } from 'src/features/explore/SearchResult' import { SEARCH_RESULT_HEADER_KEY } from './constants' // Header type used to render header text instead of SearchResult item diff --git a/apps/mobile/src/components/explore/search/utils.test.ts b/apps/mobile/src/components/explore/search/utils.test.ts index 510548e6814..e5c3dd4074b 100644 --- a/apps/mobile/src/components/explore/search/utils.test.ts +++ b/apps/mobile/src/components/explore/search/utils.test.ts @@ -4,9 +4,9 @@ import { formatTokenSearchResults, gqlNFTToNFTCollectionSearchResult, } from 'src/components/explore/search/utils' +import { SearchResultType } from 'src/features/explore/SearchResult' import { Chain, ExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks' import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { SearchResultType } from 'wallet/src/features/search/SearchResult' import { SearchTokens, TopNFTCollections } from 'wallet/src/test/gqlFixtures' type ExploreSearchResult = NonNullable diff --git a/apps/mobile/src/components/explore/search/utils.ts b/apps/mobile/src/components/explore/search/utils.ts index 0b4a806231c..75a2ad9e9b4 100644 --- a/apps/mobile/src/components/explore/search/utils.ts +++ b/apps/mobile/src/components/explore/search/utils.ts @@ -1,11 +1,11 @@ -import { Chain, ExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { searchResultId } from 'src/features/explore/searchHistorySlice' import { NFTCollectionSearchResult, SearchResultType, TokenSearchResult, -} from 'wallet/src/features/search/SearchResult' -import { searchResultId } from 'wallet/src/features/search/searchHistorySlice' +} from 'src/features/explore/SearchResult' +import { Chain, ExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { SEARCH_RESULT_HEADER_KEY } from './constants' import { SearchResultOrHeader } from './types' diff --git a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx index c70db1ba625..2d031655400 100644 --- a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx +++ b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx @@ -1,17 +1,16 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' import Trace from 'src/components/Trace/Trace' -import { MobileEventName } from 'src/features/telemetry/constants' +import { ElementName, MobileEventName } from 'src/features/telemetry/constants' import { Button, Icons } from 'ui/src' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { ElementName } from 'wallet/src/telemetry/constants' interface FiatOnRampCtaButtonProps { onPress: () => void - isLoading?: boolean + isLoading: boolean eligible: boolean disabled: boolean - analyticsProperties?: Record + analyticsProperties: Record continueButtonText: string } diff --git a/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx b/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx deleted file mode 100644 index 313c8ea2002..00000000000 --- a/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Currency } from '@uniswap/sdk-core' -import React from 'react' -import { useTranslation } from 'react-i18next' -import { StyleSheet } from 'react-native' -import { useMeldLogoUrl } from 'src/components/fiatOnRamp/hooks' -import { Loader } from 'src/components/loading' -import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks' -import { Flex, Icons, Text, TouchableArea } from 'ui/src' -import { fonts, iconSizes } from 'ui/src/theme' -import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' -import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' -import { ImageUri } from 'wallet/src/features/images/ImageUri' -import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { getSymbolDisplayText } from 'wallet/src/utils/currency' - -export function FORQuoteItem({ - quote, - serviceProvider, - currency, - loading, - baseCurrency, - onPress, - showCarret, - active, -}: { - quote: FORQuote | undefined - serviceProvider: FORServiceProvider | undefined - currency: Maybe - loading: boolean - baseCurrency: FiatCurrencyInfo - onPress: () => void - showCarret?: boolean - active?: boolean -}): JSX.Element { - const { t } = useTranslation() - const { addFiatSymbolToNumber } = useLocalizationContext() - - const quoteAmount = useFormatExactCurrencyAmount( - (quote?.destinationAmount || 0).toString(), - currency - ) - - const quoteEquivalentInSourceCurrencyAmount = addFiatSymbolToNumber({ - value: quote?.sourceAmount || 0, - currencyCode: baseCurrency.code, - currencySymbol: baseCurrency.symbol, - }) - - const logoUrl = useMeldLogoUrl(serviceProvider?.logos) - - return ( - - - {loading ? ( - - ) : ( - - - - - {serviceProvider?.name} - - - - - {quoteAmount && ( - - {t('Receive {{amount}}', { - amount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`, - })} - - )} - - {t('{{amount}} after fees', { amount: quoteEquivalentInSourceCurrencyAmount })} - - - {showCarret ? ( - - ) : ( - - )} - - { - // TODO: Enable once https://linear.app/uniswap/issue/MOB-2565/implement-service-providers-logo-once-meld-has-added-them-on-their is unblocked - false && logoUrl && ( - - } - imageStyle={ServiceProviderLogoStyles.icon} - resizeMode="contain" - uri={logoUrl} - /> - ) - } - - )} - - - ) -} - -function QuoteLoader({ showCarret }: { showCarret?: boolean }): JSX.Element { - return ( - - - - - - - - - - - {showCarret ? ( - - ) : ( - - )} - - - ) -} - -const ServiceProviderLogoStyles = StyleSheet.create({ - icon: { - height: iconSizes.icon40, - width: iconSizes.icon40, - }, -}) diff --git a/apps/mobile/src/components/fiatOnRamp/hooks.ts b/apps/mobile/src/components/fiatOnRamp/hooks.ts index 2d237df7674..03790273c04 100644 --- a/apps/mobile/src/components/fiatOnRamp/hooks.ts +++ b/apps/mobile/src/components/fiatOnRamp/hooks.ts @@ -1,4 +1,5 @@ -import { useIsDarkMode } from 'ui/src' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' + import { MeldLogos } from 'wallet/src/features/fiatOnRamp/meld' export function useMeldLogoUrl(logos: MeldLogos | undefined): string | undefined { diff --git a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx index f7e27a452f1..63e04393e6c 100644 --- a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx +++ b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx @@ -2,18 +2,18 @@ import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { BackButtonView } from 'src/components/layout/BackButtonView' import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' +import { WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' import { APP_STORE_LINK } from 'src/constants/urls' import { UpgradeStatus } from 'src/features/forceUpgrade/types' +import { ModalName } from 'src/features/telemetry/constants' +import { openUri } from 'src/utils/linking' import { Statsig } from 'statsig-react-native' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { DYNAMIC_CONFIGS } from 'wallet/src/features/experiments/constants' -import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { useNonPendingSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { ModalName } from 'wallet/src/telemetry/constants' -import { openUri } from 'wallet/src/utils/linking' export function ForceUpgradeModal(): JSX.Element { const { t } = useTranslation() diff --git a/apps/mobile/src/components/gradients/LandingBackground.tsx b/apps/mobile/src/components/gradients/LandingBackground.tsx index 72005d16607..192067e366b 100644 --- a/apps/mobile/src/components/gradients/LandingBackground.tsx +++ b/apps/mobile/src/components/gradients/LandingBackground.tsx @@ -3,10 +3,11 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { Image, Platform, ViewStyle } from 'react-native' import Rive, { Alignment, Fit, RiveRef } from 'rive-react-native' import { useAppStackNavigation } from 'src/app/navigation/types' -import { useIsDarkMode, useMedia } from 'ui/src' +import { useMedia } from 'ui/src' import { ONBOARDING_LANDING_DARK, ONBOARDING_LANDING_LIGHT } from 'ui/src/assets' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { useTimeout } from 'utilities/src/time/timing' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { Language } from 'wallet/src/features/language/constants' import { useCurrentLanguage } from 'wallet/src/features/language/hooks' import { isAndroid } from 'wallet/src/utils/platform' @@ -82,11 +83,7 @@ export const LandingBackground = (): JSX.Element | null => { } // Android 9 and 10 have issues with Rive, so we fallback on image - if ( - // Android Platform.Version is always a number - (isAndroid && typeof Platform.Version === 'number' && Platform.Version < 30) || - language !== Language.English - ) { + if ((isAndroid && Platform.Version < 30) || language !== Language.English) { return } diff --git a/apps/mobile/src/components/gradients/UniconThemedGradient.tsx b/apps/mobile/src/components/gradients/UniconThemedGradient.tsx index e5c1c0ee3fd..ba791cd46ee 100644 --- a/apps/mobile/src/components/gradients/UniconThemedGradient.tsx +++ b/apps/mobile/src/components/gradients/UniconThemedGradient.tsx @@ -1,6 +1,6 @@ import React, { memo } from 'react' import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg' -import { Tokens, getTokenValue } from 'ui/src' +import { getTokenValue, Tokens } from 'ui/src' function _UniconThemedGradient({ gradientStartColor, diff --git a/apps/mobile/src/components/home/ActivityTab.tsx b/apps/mobile/src/components/home/ActivityTab.tsx index e82732485c4..4c3338f2f11 100644 --- a/apps/mobile/src/components/home/ActivityTab.tsx +++ b/apps/mobile/src/components/home/ActivityTab.tsx @@ -3,32 +3,32 @@ import { useTranslation } from 'react-i18next' import { FlatList, RefreshControl } from 'react-native' import Animated from 'react-native-reanimated' import { useAppDispatch } from 'src/app/hooks' -import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { useAdaptiveFooter } from 'src/components/home/hooks' +import { NoTransactions } from 'src/components/icons/NoTransactions' import { AnimatedBottomSheetFlatList, AnimatedFlatList, } from 'src/components/layout/AnimatedFlatList' -import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' +import { TabProps, TAB_BAR_HEIGHT } from 'src/components/layout/TabHelpers' import { Loader } from 'src/components/loading' -import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' +import { + useCreateSwapFormState, + useMergeLocalAndRemoteTransactions, +} from 'src/features/transactions/hooks' +import TransactionSummaryLayout from 'src/features/transactions/SummaryCards/TransactionSummaryLayout' +import { useMostRecentSwapTx } from 'src/features/transactions/swap/hooks' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' -import { Flex, Icons, Text, useDeviceInsets, useSporeColors } from 'ui/src' +import { Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { GQLQueries } from 'wallet/src/data/queries' import { useFormattedTransactionDataForActivity } from 'wallet/src/features/activity/hooks' -import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' import { SwapSummaryCallbacks } from 'wallet/src/features/transactions/SummaryCards/types' import { generateActivityItemRenderer } from 'wallet/src/features/transactions/SummaryCards/utils' -import { - useCreateSwapFormState, - useMergeLocalAndRemoteTransactions, -} from 'wallet/src/features/transactions/hooks' -import { useMostRecentSwapTx } from 'wallet/src/features/transactions/swap/hooks' import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' import { useHideSpamTokensSetting } from 'wallet/src/features/wallet/hooks' -import { ModalName } from 'wallet/src/telemetry/constants' import { isAndroid } from 'wallet/src/utils/platform' export const ACTIVITY_TAB_DATA_DEPENDENCIES = [GQLQueries.TransactionList] @@ -62,9 +62,6 @@ export const ActivityTab = memo( const colors = useSporeColors() const insets = useDeviceInsets() - const { trigger: biometricsTrigger } = useBiometricPrompt() - const { requiredForTransactions: requiresBiometrics } = useBiometricAppSettings() - const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter( containerProps?.contentContainerStyle ) @@ -89,10 +86,9 @@ export const ActivityTab = memo( TransactionSummaryLayout, , SectionTitle, - swapCallbacks, - requiresBiometrics ? biometricsTrigger : undefined + swapCallbacks ) - }, [swapCallbacks, requiresBiometrics, biometricsTrigger]) + }, [swapCallbacks]) const { onRetry, hasData, isLoading, isError, sectionData, keyExtractor } = useFormattedTransactionDataForActivity( @@ -130,7 +126,7 @@ export const ActivityTab = memo( 'When you approve, trade, or transfer tokens or NFTs, your transactions will appear here.' ) } - icon={} + icon={} title={t('No activity yet')} onPress={onPressReceive} /> diff --git a/apps/mobile/src/components/home/FeedTab.tsx b/apps/mobile/src/components/home/FeedTab.tsx index 930a7ceaf8d..f6794ba32fb 100644 --- a/apps/mobile/src/components/home/FeedTab.tsx +++ b/apps/mobile/src/components/home/FeedTab.tsx @@ -3,23 +3,23 @@ import { useTranslation } from 'react-i18next' import { FlatList, RefreshControl } from 'react-native' import Animated from 'react-native-reanimated' import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { useAdaptiveFooter } from 'src/components/home/hooks' +import { NoTransactions } from 'src/components/icons/NoTransactions' import { AnimatedFlatList } from 'src/components/layout/AnimatedFlatList' -import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' +import { TabProps, TAB_BAR_HEIGHT } from 'src/components/layout/TabHelpers' import { Loader } from 'src/components/loading' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' +import TransactionSummaryLayout from 'src/features/transactions/SummaryCards/TransactionSummaryLayout' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src' -import { NoTransactions } from 'ui/src/components/icons/NoTransactions' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { GQLQueries } from 'wallet/src/data/queries' import { useFormattedTransactionDataForFeed } from 'wallet/src/features/activity/hooks' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' -import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' import { generateActivityItemRenderer } from 'wallet/src/features/transactions/SummaryCards/utils' import { useHideSpamTokensSetting } from 'wallet/src/features/wallet/hooks' -import { ModalName } from 'wallet/src/telemetry/constants' import { isAndroid } from 'wallet/src/utils/platform' export const FEED_TAB_DATA_DEPENDENCIES = [GQLQueries.FeedTransactionList] @@ -56,9 +56,7 @@ export const FeedTab = memo( return generateActivityItemRenderer( TransactionSummaryLayout, , - SectionTitle, - undefined, - undefined + SectionTitle ) }, []) diff --git a/apps/mobile/src/components/home/NftsTab.tsx b/apps/mobile/src/components/home/NftsTab.tsx index 74cdf95a1a3..226181d0958 100644 --- a/apps/mobile/src/components/home/NftsTab.tsx +++ b/apps/mobile/src/components/home/NftsTab.tsx @@ -4,17 +4,17 @@ import { RefreshControl } from 'react-native' import { useAppDispatch } from 'src/app/hooks' import { useAppStackNavigation } from 'src/app/navigation/types' import { useAdaptiveFooter } from 'src/components/home/hooks' -import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' +import { TabProps, TAB_BAR_HEIGHT } from 'src/components/layout/TabHelpers' import { NftView } from 'src/components/NFT/NftView' import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { Screens } from 'src/screens/Screens' import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' import { NftsList } from 'wallet/src/components/nfts/NftsList' import { GQLQueries } from 'wallet/src/data/queries' import { NFTItem } from 'wallet/src/features/nfts/types' -import { ModalName } from 'wallet/src/telemetry/constants' import { isAndroid } from 'wallet/src/utils/platform' export const NFTS_TAB_DATA_DEPENDENCIES = [GQLQueries.NftsTab] diff --git a/apps/mobile/src/components/home/TokensTab.tsx b/apps/mobile/src/components/home/TokensTab.tsx index a95e0ad1ce0..35074b0d9f3 100644 --- a/apps/mobile/src/components/home/TokensTab.tsx +++ b/apps/mobile/src/components/home/TokensTab.tsx @@ -8,14 +8,14 @@ import { NoTokens } from 'src/components/icons/NoTokens' import { TabContentProps, TabProps } from 'src/components/layout/TabHelpers' import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList' +import { TokenBalanceListRow } from 'src/components/TokenBalanceList/TokenBalanceListContext' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' import { Flex } from 'ui/src' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { GQLQueries } from 'wallet/src/data/queries' -import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' -import { ModalName } from 'wallet/src/telemetry/constants' import { CurrencyId } from 'wallet/src/utils/currencyId' export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances] diff --git a/apps/mobile/src/components/home/WalletEmptyState.tsx b/apps/mobile/src/components/home/WalletEmptyState.tsx index a2398838cdd..7473188bb56 100644 --- a/apps/mobile/src/components/home/WalletEmptyState.tsx +++ b/apps/mobile/src/components/home/WalletEmptyState.tsx @@ -4,12 +4,12 @@ import { useAppDispatch } from 'src/app/hooks' import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import Trace from 'src/components/Trace/Trace' import { openModal } from 'src/features/modals/modalSlice' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import PaperStackIcon from 'ui/src/assets/icons/paper-stack.svg' -import { iconSizes, colors as rawColors } from 'ui/src/theme' +import { colors as rawColors, iconSizes } from 'ui/src/theme' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { ElementName, ElementNameType, ModalName } from 'wallet/src/telemetry/constants' import { opacify } from 'wallet/src/utils/colors' interface ActionCardItem { @@ -17,7 +17,7 @@ interface ActionCardItem { blurb: string icon: JSX.Element onPress: () => void - elementName: ElementNameType + elementName: ElementName badgeText?: string } diff --git a/packages/wallet/src/components/icons/Arrow.tsx b/apps/mobile/src/components/icons/Arrow.tsx similarity index 96% rename from packages/wallet/src/components/icons/Arrow.tsx rename to apps/mobile/src/components/icons/Arrow.tsx index e9417007baa..1b226aa6096 100644 --- a/packages/wallet/src/components/icons/Arrow.tsx +++ b/apps/mobile/src/components/icons/Arrow.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import React, { memo } from 'react' import ArrowDown from 'ui/src/assets/icons/arrow-down.svg' type Props = { diff --git a/apps/mobile/src/components/icons/BlockExplorerIcon.tsx b/apps/mobile/src/components/icons/BlockExplorerIcon.tsx index 10db05edc71..58a23dd6a77 100644 --- a/apps/mobile/src/components/icons/BlockExplorerIcon.tsx +++ b/apps/mobile/src/components/icons/BlockExplorerIcon.tsx @@ -1,8 +1,8 @@ import React from 'react' import { SvgProps } from 'react-native-svg' -import { useIsDarkMode } from 'ui/src' import { IconSizeTokens } from 'ui/src/theme' -import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' type IconComponentProps = SvgProps & { size?: IconSizeTokens | number } diff --git a/packages/wallet/src/components/icons/CheckmarkCircle.tsx b/apps/mobile/src/components/icons/CheckmarkCircle.tsx similarity index 95% rename from packages/wallet/src/components/icons/CheckmarkCircle.tsx rename to apps/mobile/src/components/icons/CheckmarkCircle.tsx index 097e3225e27..7c305673b5f 100644 --- a/packages/wallet/src/components/icons/CheckmarkCircle.tsx +++ b/apps/mobile/src/components/icons/CheckmarkCircle.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import React, { memo } from 'react' import { Flex, FlexProps, useSporeColors } from 'ui/src' import Checkmark from 'ui/src/assets/icons/checkmark.svg' diff --git a/apps/mobile/src/components/icons/NoTransactions.tsx b/apps/mobile/src/components/icons/NoTransactions.tsx new file mode 100644 index 00000000000..26119935544 --- /dev/null +++ b/apps/mobile/src/components/icons/NoTransactions.tsx @@ -0,0 +1,19 @@ +import React, { memo } from 'react' +import OverlayIcon from 'src/components/icons/OverlayIcon' +import { Flex, useSporeColors } from 'ui/src' +import NoTransactionFgIcon from 'ui/src/assets/icons/empty-state-coin.svg' +import NoTransactionBgIcon from 'ui/src/assets/icons/empty-state-transaction.svg' + +export const NoTransactions = memo(function _NoTransactions() { + const colors = useSporeColors() + return ( + + } + left={5} + overlay={} + /> + + ) +}) diff --git a/packages/wallet/src/components/input/AmountInput.test.tsx b/apps/mobile/src/components/input/AmountInput.test.tsx similarity index 98% rename from packages/wallet/src/components/input/AmountInput.test.tsx rename to apps/mobile/src/components/input/AmountInput.test.tsx index 0db833309a9..f67b1909938 100644 --- a/packages/wallet/src/components/input/AmountInput.test.tsx +++ b/apps/mobile/src/components/input/AmountInput.test.tsx @@ -1,4 +1,4 @@ -import { parseValue, replaceSeparators } from './AmountInput' +import { parseValue, replaceSeparators } from 'src/components/input/AmountInput' describe(replaceSeparators, () => { describe('it can strip grouping separators', () => { diff --git a/packages/wallet/src/components/input/AmountInput.tsx b/apps/mobile/src/components/input/AmountInput.tsx similarity index 88% rename from packages/wallet/src/components/input/AmountInput.tsx rename to apps/mobile/src/components/input/AmountInput.tsx index 35bc62abcc4..bb96a7ea703 100644 --- a/packages/wallet/src/components/input/AmountInput.tsx +++ b/apps/mobile/src/components/input/AmountInput.tsx @@ -1,15 +1,15 @@ -import { forwardRef, useCallback, useEffect, useMemo } from 'react' +import React, { forwardRef, useCallback, useEffect, useMemo } from 'react' import { AppState, Keyboard, KeyboardTypeOptions, TextInput as NativeTextInput } from 'react-native' import { getNumberFormatSettings } from 'react-native-localize' -import { TextInput, TextInputProps } from 'wallet/src/components/input/TextInput' -import { FiatCurrencyInfo, useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { TextInput, TextInputProps } from 'src/components/input/TextInput' +import { useMoonpayFiatCurrencySupportInfo } from 'src/features/fiatOnRamp/hooks' +import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' const numericInputRegex = RegExp('^\\d*(\\.\\d*)?$') // Matches only numeric values without commas type Props = { showCurrencySign: boolean - fiatCurrencyInfo?: FiatCurrencyInfo dimTextColor?: boolean maxDecimals?: number } & TextInputProps @@ -94,7 +94,6 @@ export const AmountInput = forwardRef(function _AmountIn dimTextColor, showSoftInputOnFocus, maxDecimals, - fiatCurrencyInfo, ...rest }, ref @@ -105,7 +104,7 @@ export const AmountInput = forwardRef(function _AmountIn const invalidInput = value && !numericInputRegex.test(value) useEffect(() => { - // Resets input if non-numeric value is passed + // Resets input if non-numberic value is passed if (invalidInput) { onChangeText?.('') } @@ -136,6 +135,7 @@ export const AmountInput = forwardRef(function _AmountIn ] ) + const { moonpaySupportedFiatCurrency: currency } = useMoonpayFiatCurrencySupportInfo() const { addFiatSymbolToNumber } = useLocalizationContext() let formattedValue = replaceSeparators({ @@ -145,14 +145,13 @@ export const AmountInput = forwardRef(function _AmountIn groupingOverride: '', decimalOverride: decimalSeparator, }) - formattedValue = - showCurrencySign && fiatCurrencyInfo - ? addFiatSymbolToNumber({ - value: formattedValue, - currencyCode: fiatCurrencyInfo.code, - currencySymbol: fiatCurrencyInfo.symbol, - }) - : formattedValue + formattedValue = showCurrencySign + ? addFiatSymbolToNumber({ + value: formattedValue, + currencyCode: currency.code, + currencySymbol: currency.symbol, + }) + : formattedValue const textInputProps: TextInputProps = useMemo( () => ({ @@ -172,7 +171,7 @@ export const AmountInput = forwardRef(function _AmountIn return } // Dismiss keyboard when app is foregrounded (showSoftInputOnFocus doesn't - // work when the app activates from the background) + // wotk when the app activates from the background) Keyboard.dismiss() }) diff --git a/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx b/apps/mobile/src/components/input/CurrencyInputPanel.tsx similarity index 93% rename from packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx rename to apps/mobile/src/components/input/CurrencyInputPanel.tsx index 6c9dbe18288..fa13d010df3 100644 --- a/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx +++ b/apps/mobile/src/components/input/CurrencyInputPanel.tsx @@ -1,5 +1,5 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { memo, useCallback, useEffect, useMemo, useRef } from 'react' +import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { NativeSyntheticEvent, @@ -7,16 +7,16 @@ import { TextInputProps, TextInputSelectionChangeEventData, } from 'react-native' +import { useDynamicFontSizing } from 'src/app/hooks' +import { AmountInput } from 'src/components/input/AmountInput' +import { MaxAmountButton } from 'src/components/input/MaxAmountButton' +import { Warning, WarningLabel } from 'src/components/modals/WarningModal/types' +import { SelectTokenButton } from 'src/components/TokenSelector/SelectTokenButton' import { Flex, FlexProps, SpaceTokens, Text } from 'ui/src' import { fonts } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' -import { AmountInput } from 'wallet/src/components/input/AmountInput' -import { MaxAmountButton } from 'wallet/src/components/input/MaxAmountButton' -import { SelectTokenButton } from 'wallet/src/components/TokenSelector/SelectTokenButton' import { CurrencyInfo } from 'wallet/src/features/dataApi/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { Warning, WarningLabel } from 'wallet/src/features/transactions/WarningModal/types' -import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' type CurrentInputPanelProps = { currencyInfo: Maybe @@ -285,9 +285,4 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element ) } -/** - * @deprecated - * Flows should use the new CurrencyInputPanel component instead of this one. - * Requires small changes in the props. - */ -export const CurrencyInputPanelLegacy = memo(_CurrencyInputPanel) +export const CurrencyInputPanel = memo(_CurrencyInputPanel) diff --git a/packages/wallet/src/components/legacy/DecimalPadLegacy.tsx b/apps/mobile/src/components/input/DecimalPad.tsx similarity index 95% rename from packages/wallet/src/components/legacy/DecimalPadLegacy.tsx rename to apps/mobile/src/components/input/DecimalPad.tsx index bc30059450c..c9009c4f31b 100644 --- a/packages/wallet/src/components/legacy/DecimalPadLegacy.tsx +++ b/apps/mobile/src/components/input/DecimalPad.tsx @@ -1,5 +1,5 @@ import { ImpactFeedbackStyle } from 'expo-haptics' -import { memo, useMemo } from 'react' +import React, { memo, useMemo } from 'react' import { I18nManager, TextInputProps } from 'react-native' import { AnimatedFlex, Flex, Icons, Text, TouchableArea } from 'ui/src' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' @@ -43,11 +43,6 @@ interface DecimalPadProps { hasCurrencyPrefix?: boolean } -/** - * TODO: - * This component is depracted and should be replaced with DecimalPadInput, which is being used - * int he new swap components. - */ export function _DecimalPad({ setValue, value = '', @@ -243,8 +238,4 @@ function KeyButton({ ) } -/** - * @deprecated Use DecimalPadInput instead. - * Mostly similar but requires a few prop changes. - */ -export const DecimalPadLegacy = memo(_DecimalPad) +export const DecimalPad = memo(_DecimalPad) diff --git a/packages/wallet/src/components/input/MaxAmountButton.tsx b/apps/mobile/src/components/input/MaxAmountButton.tsx similarity index 87% rename from packages/wallet/src/components/input/MaxAmountButton.tsx rename to apps/mobile/src/components/input/MaxAmountButton.tsx index b05f19bd2e3..0a7d3532f7a 100644 --- a/packages/wallet/src/components/input/MaxAmountButton.tsx +++ b/apps/mobile/src/components/input/MaxAmountButton.tsx @@ -1,10 +1,11 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import React from 'react' import { useTranslation } from 'react-i18next' import { StyleProp, ViewStyle } from 'react-native' +import Trace from 'src/components/Trace/Trace' +import { ElementName } from 'src/features/telemetry/constants' +import { maxAmountSpend } from 'src/utils/balance' import { Text, TouchableArea } from 'ui/src' -import { Trace } from 'utilities/src/telemetry/trace/Trace' -import { ElementName } from 'wallet/src/telemetry/constants' -import { maxAmountSpend } from 'wallet/src/utils/balance' interface MaxAmountButtonProps { currencyAmount: CurrencyAmount | null | undefined diff --git a/apps/mobile/src/components/input/PasswordInput.tsx b/apps/mobile/src/components/input/PasswordInput.tsx index c20427024e6..4a909f9c258 100644 --- a/apps/mobile/src/components/input/PasswordInput.tsx +++ b/apps/mobile/src/components/input/PasswordInput.tsx @@ -1,10 +1,10 @@ import React, { forwardRef, useState } from 'react' import { TextInput as NativeTextInput } from 'react-native' +import { TextInput, TextInputProps } from 'src/components/input/TextInput' import { AnimatedFlex, Flex, TouchableArea, useSporeColors } from 'ui/src' import EyeOffIcon from 'ui/src/assets/icons/eye-off.svg' import EyeIcon from 'ui/src/assets/icons/eye.svg' import { iconSizes } from 'ui/src/theme' -import { TextInput, TextInputProps } from 'wallet/src/components/input/TextInput' export const PasswordInput = forwardRef(function _PasswordInput( props, @@ -23,11 +23,10 @@ export const PasswordInput = forwardRef(functio + borderRadius="$rounded12" + borderWidth={1}> - + diff --git a/packages/wallet/src/features/search/SearchTextInput.tsx b/apps/mobile/src/components/input/SearchTextInput.tsx similarity index 95% rename from packages/wallet/src/features/search/SearchTextInput.tsx rename to apps/mobile/src/components/input/SearchTextInput.tsx index ab556b061ef..d656f8d7be1 100644 --- a/packages/wallet/src/features/search/SearchTextInput.tsx +++ b/apps/mobile/src/components/input/SearchTextInput.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback, useState } from 'react' +import React, { forwardRef, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Keyboard, @@ -7,6 +7,8 @@ import { TextInput as NativeTextInput, TextInputFocusEventData, } from 'react-native' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' import { AnimatePresence, Button, @@ -21,8 +23,6 @@ import { } from 'ui/src' import { fonts, spacing } from 'ui/src/theme' import { SHADOW_OFFSET_SMALL } from 'wallet/src/components/BaseCard/BaseCard' -import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' -import { WalletEventName } from 'wallet/src/telemetry/constants' export const springConfig = { stiffness: 1000, @@ -71,7 +71,7 @@ export const SearchTextInput = forwardRef setIsFocus(false) setShowClearButton(false) Keyboard.dismiss() - sendWalletAnalyticsEvent(WalletEventName.ExploreSearchCancel, { query: value }) + sendMobileAnalyticsEvent(MobileEventName.ExploreSearchCancel, { query: value }) onChangeText?.('') onCancel?.() } diff --git a/packages/wallet/src/components/input/TextInput.tsx b/apps/mobile/src/components/input/TextInput.tsx similarity index 95% rename from packages/wallet/src/components/input/TextInput.tsx rename to apps/mobile/src/components/input/TextInput.tsx index a6d2193836a..b2cacf81ab2 100644 --- a/packages/wallet/src/components/input/TextInput.tsx +++ b/apps/mobile/src/components/input/TextInput.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from 'react' +import React, { forwardRef } from 'react' import { TextInput as TextInputBase } from 'react-native' import { Input, InputProps, useSporeColors } from 'ui/src' diff --git a/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap index 3bc395c4674..f8f9feaa7b6 100644 --- a/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap +++ b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap @@ -28,6 +28,7 @@ exports[`renders selection circle 1`] = ` { rotation.value = withRepeat( diff --git a/packages/ui/src/loading/TokenLoader.tsx b/apps/mobile/src/components/loading/TokenLoader.tsx similarity index 88% rename from packages/ui/src/loading/TokenLoader.tsx rename to apps/mobile/src/components/loading/TokenLoader.tsx index 3f0e8782490..f5f7abea82b 100644 --- a/packages/ui/src/loading/TokenLoader.tsx +++ b/apps/mobile/src/components/loading/TokenLoader.tsx @@ -1,13 +1,12 @@ -import { Flex } from 'ui/src/components/layout' -import { Text } from 'ui/src/components/text' +import React from 'react' +import { Flex, Text } from 'ui/src' import { iconSizes } from 'ui/src/theme' +import { TOKEN_BALANCE_ITEM_HEIGHT } from 'wallet/src/features/portfolio/TokenBalanceItem' interface TokenLoaderProps { opacity: number } -const TOKEN_BALANCE_ITEM_HEIGHT = 56 - export function TokenLoader({ opacity }: TokenLoaderProps): JSX.Element { return ( + + {new Array(repeat).fill(null).map((_, i, { length }) => ( + + + + ))} + + + ) +} + export const Transaction = memo(function _Transaction({ repeat = 1, }: { @@ -71,6 +86,7 @@ function Favorite({ height, contrast }: { height?: number; contrast?: boolean }) export const Loader = { Box, + Token, Transaction, Wallets, Graph, diff --git a/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx index 8af6474b970..349283bee1a 100644 --- a/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx +++ b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx @@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next' import { usePrevious } from 'react-native-wagmi-charts' import { HiddenMnemonicWordView } from 'src/components/mnemonic/HiddenMnemonicWordView' import { MnemonicDisplay } from 'src/components/mnemonic/MnemonicDisplay' +import { WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { useWalletRestore } from 'src/features/wallet/hooks' import { Button, Flex } from 'ui/src' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' type Props = { mnemonicId: string diff --git a/packages/wallet/src/components/modals/ActionSheetModal.tsx b/apps/mobile/src/components/modals/ActionSheetModal.tsx similarity index 91% rename from packages/wallet/src/components/modals/ActionSheetModal.tsx rename to apps/mobile/src/components/modals/ActionSheetModal.tsx index 6039e6100c5..93c1045a69c 100644 --- a/packages/wallet/src/components/modals/ActionSheetModal.tsx +++ b/apps/mobile/src/components/modals/ActionSheetModal.tsx @@ -1,9 +1,9 @@ -import { ReactNode } from 'react' +import React, { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native-gesture-handler' +import { BottomSheetDetachedModal } from 'src/components/modals/BottomSheetModal' +import { ModalName } from 'src/features/telemetry/constants' import { Flex, flexStyles, Text, TouchableArea, useDeviceDimensions } from 'ui/src' -import { BottomSheetDetachedModal } from 'wallet/src/components/modals/BottomSheetModal' -import { ModalNameType } from 'wallet/src/telemetry/constants' export interface MenuItemProp { key: string @@ -62,7 +62,7 @@ export function ActionSheetModalContent(props: ActionSheetModalContentProps): JS interface ActionSheetModalProps extends ActionSheetModalContentProps { isVisible: boolean - name: ModalNameType + name: ModalName } export function ActionSheetModal({ diff --git a/packages/wallet/src/components/modals/BottomSheetContext.tsx b/apps/mobile/src/components/modals/BottomSheetContext.tsx similarity index 100% rename from packages/wallet/src/components/modals/BottomSheetContext.tsx rename to apps/mobile/src/components/modals/BottomSheetContext.tsx diff --git a/apps/mobile/src/components/modals/BottomSheetModal.tsx b/apps/mobile/src/components/modals/BottomSheetModal.tsx new file mode 100644 index 00000000000..0499fb2c8e3 --- /dev/null +++ b/apps/mobile/src/components/modals/BottomSheetModal.tsx @@ -0,0 +1,438 @@ +import { + BottomSheetBackdrop, + BottomSheetBackdropProps, + BottomSheetHandleProps, + BottomSheetModal as BaseModal, + BottomSheetView, + useBottomSheetDynamicSnapPoints, +} from '@gorhom/bottom-sheet' +import { BlurView } from 'expo-blur' +import React, { + ComponentProps, + forwardRef, + PropsWithChildren, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react' +import { + BackHandler, + Keyboard, + LayoutChangeEvent, + StyleProp, + StyleSheet, + ViewStyle, +} from 'react-native' +import Animated, { + Extrapolate, + interpolate, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated' +import { BottomSheetContextProvider } from 'src/components/modals/BottomSheetContext' +import { HandleBar } from 'src/components/modals/HandleBar' +import Trace from 'src/components/Trace/Trace' +import { ModalName } from 'src/features/telemetry/constants' +import { useKeyboardLayout } from 'src/utils/useKeyboardLayout' +import { + DynamicColor, + Flex, + useDeviceDimensions, + useDeviceInsets, + useMedia, + useSporeColors, +} from 'ui/src' +import { borderRadii, spacing } from 'ui/src/theme' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' +import { isAndroid, isIOS } from 'wallet/src/utils/platform' + +/** + * (android only) + * Adds a back handler to the modal that dismisses it when the back button is pressed. + * + * @param modalRef - ref to the modal + * @param enabled - whether to enable the back handler + */ +function useModalBackHandler(modalRef: React.RefObject, enabled: boolean): void { + useEffect(() => { + if (enabled) { + const subscription = BackHandler.addEventListener('hardwareBackPress', () => { + modalRef.current?.close() + return true + }) + + return subscription.remove + } + }, [modalRef, enabled]) +} + +type Props = PropsWithChildren<{ + animatedPosition?: Animated.SharedValue + hideHandlebar?: boolean + name: ModalName + onClose?: () => void + snapPoints?: Array + stackBehavior?: ComponentProps['stackBehavior'] + containerComponent?: ComponentProps['containerComponent'] + footerComponent?: ComponentProps['footerComponent'] + fullScreen?: boolean + backgroundColor?: DynamicColor + blurredBackground?: boolean + dismissOnBackPress?: boolean + isDismissible?: boolean + overrideInnerContainer?: boolean + renderBehindTopInset?: boolean + renderBehindBottomInset?: boolean + hideKeyboardOnDismiss?: boolean + hideKeyboardOnSwipeDown?: boolean + // extend the sheet to its maximum snap point when keyboard is visible + extendOnKeyboardVisible?: boolean +}> + +const BACKDROP_APPEARS_ON_INDEX = 0 +const DISAPPEARS_ON_INDEX = -1 +const DRAG_ACTIVATION_OFFSET = 40 + +const Backdrop = (props: BottomSheetBackdropProps): JSX.Element => { + return ( + + ) +} + +const CONTENT_HEIGHT_SNAP_POINTS = ['CONTENT_HEIGHT'] + +export type BottomSheetModalRef = { + handleContentLayout: (event: LayoutChangeEvent) => void +} + +export const BottomSheetModal = forwardRef(function BottomSheetModal( + { + children, + name, + onClose, + snapPoints = CONTENT_HEIGHT_SNAP_POINTS, + stackBehavior = 'push', + animatedPosition: providedAnimatedPosition, + containerComponent, + footerComponent, + fullScreen, + hideHandlebar, + backgroundColor, + blurredBackground = false, + dismissOnBackPress = true, + isDismissible = true, + overrideInnerContainer = false, + renderBehindTopInset = false, + renderBehindBottomInset = false, + hideKeyboardOnDismiss = false, + hideKeyboardOnSwipeDown = false, + // keyboardBehavior="extend" does not work and it's hard to figure why, + // probably it requires usage of + extendOnKeyboardVisible = false, + }, + ref +): JSX.Element { + const dimensions = useDeviceDimensions() + const insets = useDeviceInsets() + const modalRef = useRef(null) + const keyboard = useKeyboardLayout() + + const { animatedHandleHeight, animatedSnapPoints, animatedContentHeight, handleContentLayout } = + useBottomSheetDynamicSnapPoints(snapPoints) + const [isSheetReady, setIsSheetReady] = useState(false) + + useModalBackHandler(modalRef, isDismissible && dismissOnBackPress) + + useEffect(() => { + modalRef.current?.present() + // Close modal when it is unmounted + return modalRef.current?.close + }, [modalRef]) + + useEffect(() => { + if (extendOnKeyboardVisible && keyboard.isVisible) { + modalRef.current?.expand() + } + }, [extendOnKeyboardVisible, keyboard.isVisible]) + + const internalAnimatedPosition = useSharedValue(0) + const animatedPosition = providedAnimatedPosition ?? internalAnimatedPosition + + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const media = useMedia() + + const backgroundColorValue = blurredBackground + ? colors.transparent.val + : backgroundColor ?? colors.surface1.get() + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [blurredBackground, isDismissible] + ) + + const renderHandleBar = useCallback( + (props: BottomSheetHandleProps) => { + // This adds an extra gap of unwanted space + if (renderBehindTopInset && hideHandlebar) { + return null + } + return ( + @@ -93,7 +113,7 @@ export function WarningModal({ fill testID={ElementName.Confirm} theme={alertColor.buttonTheme} - onPress={onConfirm}> + onPress={onPressConfirm}> {confirmText} )} diff --git a/packages/wallet/src/features/transactions/WarningModal/getNetworkWarning.ts b/apps/mobile/src/components/modals/WarningModal/constants.ts similarity index 90% rename from packages/wallet/src/features/transactions/WarningModal/getNetworkWarning.ts rename to apps/mobile/src/components/modals/WarningModal/constants.ts index 3f673ae9d73..f218b2435ff 100644 --- a/packages/wallet/src/features/transactions/WarningModal/getNetworkWarning.ts +++ b/apps/mobile/src/components/modals/WarningModal/constants.ts @@ -1,11 +1,11 @@ -import WifiIcon from 'ui/src/assets/icons/wifi-slash.svg' -import { AppTFunction } from 'ui/src/i18n/types' import { Warning, WarningAction, WarningLabel, WarningSeverity, -} from 'wallet/src/features/transactions/WarningModal/types' +} from 'src/components/modals/WarningModal/types' +import WifiIcon from 'ui/src/assets/icons/wifi-slash.svg' +import { AppTFunction } from 'ui/src/i18n/types' export const getNetworkWarning = (t: AppTFunction): Warning => ({ type: WarningLabel.NetworkError, diff --git a/packages/wallet/src/features/transactions/WarningModal/types.ts b/apps/mobile/src/components/modals/WarningModal/types.ts similarity index 100% rename from packages/wallet/src/features/transactions/WarningModal/types.ts rename to apps/mobile/src/components/modals/WarningModal/types.ts diff --git a/packages/wallet/src/components/modals/hooks.ts b/apps/mobile/src/components/modals/hooks.ts similarity index 100% rename from packages/wallet/src/components/modals/hooks.ts rename to apps/mobile/src/components/modals/hooks.ts diff --git a/apps/mobile/src/components/notifications/Badge.tsx b/apps/mobile/src/components/notifications/Badge.tsx index ddfeab6a0d2..2477a64ca4b 100644 --- a/apps/mobile/src/components/notifications/Badge.tsx +++ b/apps/mobile/src/components/notifications/Badge.tsx @@ -1,6 +1,6 @@ import React, { memo, PropsWithChildren } from 'react' +import { useSelectAddressHasNotifications } from 'src/features/notifications/hooks' import { Flex } from 'ui/src' -import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks' type Props = PropsWithChildren<{ address: Address diff --git a/apps/mobile/src/components/sortableGrid/SortableGrid.tsx b/apps/mobile/src/components/sortableGrid/SortableGrid.tsx index 5224ada1f47..c2441572e5f 100644 --- a/apps/mobile/src/components/sortableGrid/SortableGrid.tsx +++ b/apps/mobile/src/components/sortableGrid/SortableGrid.tsx @@ -1,9 +1,9 @@ import { memo, useRef } from 'react' import { LayoutChangeEvent, MeasureLayoutOnSuccessCallback, View } from 'react-native' import { Flex, FlexProps } from 'ui/src' +import { useStableCallback } from './hooks' import SortableGridItem from './SortableGridItem' import SortableGridProvider, { useSortableGridContext } from './SortableGridProvider' -import { useStableCallback } from './hooks' import { AutoScrollProps, SortableGridChangeEvent, SortableGridRenderItem } from './types' import { defaultKeyExtractor } from './utils' diff --git a/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx b/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx index e631e9f2967..40db9a18768 100644 --- a/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx +++ b/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx @@ -12,9 +12,9 @@ import Animated, { withTiming, } from 'react-native-reanimated' import ActiveItemDecoration from './ActiveItemDecoration' -import { useSortableGridContext } from './SortableGridProvider' import { TIME_TO_ACTIVATE_PAN } from './constants' import { useAnimatedZIndex, useItemOrderUpdater } from './hooks' +import { useSortableGridContext } from './SortableGridProvider' import { SortableGridRenderItem } from './types' type SortableGridItemProps = { diff --git a/apps/mobile/src/components/sortableGrid/hooks.ts b/apps/mobile/src/components/sortableGrid/hooks.ts index 0536737186a..473bf9f89fb 100644 --- a/apps/mobile/src/components/sortableGrid/hooks.ts +++ b/apps/mobile/src/components/sortableGrid/hooks.ts @@ -1,8 +1,8 @@ import { useCallback, useRef } from 'react' import { FlatList, ScrollView } from 'react-native' -import { SharedValue, runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated' -import { useSortableGridContext } from './SortableGridProvider' +import { runOnJS, SharedValue, useAnimatedReaction, useSharedValue } from 'react-native-reanimated' import { AUTO_SCROLL_THRESHOLD } from './constants' +import { useSortableGridContext } from './SortableGridProvider' import { ItemMeasurements } from './types' export function useStableCallback< diff --git a/apps/mobile/src/components/text/AnimatedText.test.tsx b/apps/mobile/src/components/text/AnimatedText.test.tsx index a8054984170..5136e2db5ef 100644 --- a/apps/mobile/src/components/text/AnimatedText.test.tsx +++ b/apps/mobile/src/components/text/AnimatedText.test.tsx @@ -1,13 +1,12 @@ -import { fireEvent } from '@testing-library/react-native' +import { fireEvent, render } from '@testing-library/react-native' import React from 'react' import { makeMutable } from 'react-native-reanimated' import { act } from 'react-test-renderer' import { AnimatedText } from 'src/components/text/AnimatedText' -import { renderWithProviders } from 'src/test/render' describe(AnimatedText, () => { it('renders without error', () => { - const tree = renderWithProviders() + const tree = render() expect(tree).toMatchInlineSnapshot(` { describe('when text is in the loading state', () => { it('displays text placeholder with loading shimmer when the loading property is true', async () => { - const tree = renderWithProviders() + const tree = render() const shimmerPlaceholder = tree.getByTestId('shimmer-placeholder') @@ -58,7 +57,7 @@ describe(AnimatedText, () => { }) it('displays the loading placeholder without shimmer when the loading property has "no-shimmer" value', () => { - const tree = renderWithProviders() + const tree = render() const shimmerPlaceholder = tree.queryByTestId('shimmer-placeholder') expect(shimmerPlaceholder).toBeFalsy() @@ -74,7 +73,7 @@ describe(AnimatedText, () => { describe('when text is not in the loading state', () => { it('updates text when text value is modified', async () => { const textValue = makeMutable('Initial') - const tree = renderWithProviders() + const tree = render() expect(tree.queryByDisplayValue('Initial')).toBeTruthy() diff --git a/apps/mobile/src/components/text/AnimatedText.tsx b/apps/mobile/src/components/text/AnimatedText.tsx index a1a12bbc169..e7dba9a511e 100644 --- a/apps/mobile/src/components/text/AnimatedText.tsx +++ b/apps/mobile/src/components/text/AnimatedText.tsx @@ -1,13 +1,13 @@ import React from 'react' import { - TextProps as RNTextProps, StyleSheet, TextInput, TextInputProps, + TextProps as RNTextProps, useWindowDimensions, } from 'react-native' import Animated, { useAnimatedProps } from 'react-native-reanimated' -import { Flex, TextProps as TamaTextProps, TextFrame, usePropsAndStyle } from 'ui/src' +import { Flex, TextFrame, TextProps as TamaTextProps, usePropsAndStyle } from 'ui/src' import { TextLoaderWrapper } from 'ui/src/components/text/Text' import { fonts } from 'ui/src/theme' diff --git a/packages/wallet/src/components/text/LearnMoreLink.tsx b/apps/mobile/src/components/text/LearnMoreLink.tsx similarity index 90% rename from packages/wallet/src/components/text/LearnMoreLink.tsx rename to apps/mobile/src/components/text/LearnMoreLink.tsx index 8a693846d9f..aa7d52b6b85 100644 --- a/packages/wallet/src/components/text/LearnMoreLink.tsx +++ b/apps/mobile/src/components/text/LearnMoreLink.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' +import { openUri } from 'src/utils/linking' import { Text, TouchableArea } from 'ui/src' -import { openUri } from 'wallet/src/utils/linking' const onPressLearnMore = async (url: string): Promise => { await openUri(url) diff --git a/apps/mobile/src/components/text/LongMarkdownText.test.tsx b/apps/mobile/src/components/text/LongMarkdownText.test.tsx index c6adee187cd..f7dd5401f79 100644 --- a/apps/mobile/src/components/text/LongMarkdownText.test.tsx +++ b/apps/mobile/src/components/text/LongMarkdownText.test.tsx @@ -101,6 +101,7 @@ describe(LongMarkdownText, () => { const readMoreButton = tree.queryByTestId('read-more-button') expect(readMoreButton).toBeTruthy() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(within(readMoreButton!).getByText('Read more')).toBeTruthy() }) }) @@ -128,7 +129,7 @@ describe(LongMarkdownText, () => { fireEvent.press(readMoreButton) expect(readMoreButton).toBeTruthy() - + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(within(readMoreButton!).getByText('Read less')).toBeTruthy() }) }) diff --git a/apps/mobile/src/components/text/LongMarkdownText.tsx b/apps/mobile/src/components/text/LongMarkdownText.tsx index 79fe4a81b8d..362c712adf9 100644 --- a/apps/mobile/src/components/text/LongMarkdownText.tsx +++ b/apps/mobile/src/components/text/LongMarkdownText.tsx @@ -2,9 +2,9 @@ import React, { useCallback, useReducer, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LayoutChangeEvent } from 'react-native' import Markdown, { MarkdownProps } from 'react-native-markdown-display' +import { openUri } from 'src/utils/linking' import { Flex, SpaceTokens, Text, useSporeColors } from 'ui/src' import { fonts } from 'ui/src/theme' -import { openUri } from 'wallet/src/utils/linking' type LongMarkdownTextProps = { initialDisplayedLines?: number diff --git a/apps/mobile/src/components/text/LongText.test.tsx b/apps/mobile/src/components/text/LongText.test.tsx index 4ace94ece88..821b2a41b20 100644 --- a/apps/mobile/src/components/text/LongText.test.tsx +++ b/apps/mobile/src/components/text/LongText.test.tsx @@ -66,6 +66,7 @@ describe(LongText, () => { const readMoreButton = tree.queryByTestId('read-more-button') expect(readMoreButton).toBeTruthy() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(within(readMoreButton!).getByText('Read more')).toBeTruthy() }) }) diff --git a/packages/wallet/src/components/text/Pill.test.tsx b/apps/mobile/src/components/text/Pill.test.tsx similarity index 78% rename from packages/wallet/src/components/text/Pill.test.tsx rename to apps/mobile/src/components/text/Pill.test.tsx index 6c12904aebf..0a8da661d94 100644 --- a/packages/wallet/src/components/text/Pill.test.tsx +++ b/apps/mobile/src/components/text/Pill.test.tsx @@ -1,6 +1,7 @@ +import React from 'react' +import { Pill } from 'src/components/text/Pill' +import { render } from 'src/test/test-utils' import { Text } from 'ui/src' -import { render } from 'wallet/src/test/test-utils' -import { Pill } from './Pill' it('renders a Pill without image', () => { const tree = render( diff --git a/packages/wallet/src/components/text/Pill.tsx b/apps/mobile/src/components/text/Pill.tsx similarity index 95% rename from packages/wallet/src/components/text/Pill.tsx rename to apps/mobile/src/components/text/Pill.tsx index 16dfb725d61..a693fd14bdd 100644 --- a/packages/wallet/src/components/text/Pill.tsx +++ b/apps/mobile/src/components/text/Pill.tsx @@ -28,7 +28,7 @@ export function Pill({ return ( , t: AppTFunction): string { switch (safetyLevel) { diff --git a/packages/wallet/src/components/icons/WarningIcon.tsx b/apps/mobile/src/components/tokens/WarningIcon.tsx similarity index 89% rename from packages/wallet/src/components/icons/WarningIcon.tsx rename to apps/mobile/src/components/tokens/WarningIcon.tsx index ed17db78912..d009e3dae21 100644 --- a/packages/wallet/src/components/icons/WarningIcon.tsx +++ b/apps/mobile/src/components/tokens/WarningIcon.tsx @@ -1,9 +1,10 @@ +import React from 'react' import { SvgProps } from 'react-native-svg' +import { useTokenSafetyLevelColors } from 'src/features/tokens/safetyHooks' import { useSporeColors } from 'ui/src' import AlertTriangle from 'ui/src/assets/icons/alert-triangle.svg' import XOctagon from 'ui/src/assets/icons/x-octagon.svg' import { SafetyLevel } from 'wallet/src/data/__generated__/types-and-hooks' -import { useTokenSafetyLevelColors } from 'wallet/src/features/tokens/safetyHooks' interface Props { safetyLevel: Maybe diff --git a/packages/wallet/src/features/tokens/utils.ts b/apps/mobile/src/components/tokens/utils.ts similarity index 100% rename from packages/wallet/src/features/tokens/utils.ts rename to apps/mobile/src/components/tokens/utils.ts diff --git a/apps/mobile/src/components/tooltip/TooltipButton.tsx b/apps/mobile/src/components/tooltip/TooltipButton.tsx index 67328552951..b659d81d4d7 100644 --- a/apps/mobile/src/components/tooltip/TooltipButton.tsx +++ b/apps/mobile/src/components/tooltip/TooltipButton.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import { ColorValue, Keyboard } from 'react-native' +import { ColorValue } from 'react-native' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' +import { ModalName } from 'src/features/telemetry/constants' import { TouchableArea, TouchableAreaProps, useSporeColors } from 'ui/src' import InfoCircle from 'ui/src/assets/icons/info-circle.svg' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { ModalName } from 'wallet/src/telemetry/constants' const DEFAULT_ICON_SIZE = 20 @@ -33,12 +33,7 @@ export function TooltipInfoButton({ const { t } = useTranslation() return ( <> - { - Keyboard.dismiss() - setShowModal(true) - }} - {...rest}> + setShowModal(true)} {...rest}> { const tree = render( diff --git a/packages/ui/src/components/Unicon/index.native.tsx b/apps/mobile/src/components/unicons/Unicon.tsx similarity index 95% rename from packages/ui/src/components/Unicon/index.native.tsx rename to apps/mobile/src/components/unicons/Unicon.tsx index 5a33bf05a4b..d1d272bcb6f 100644 --- a/packages/ui/src/components/Unicon/index.native.tsx +++ b/apps/mobile/src/components/unicons/Unicon.tsx @@ -10,17 +10,21 @@ import { Rect, vec, } from '@shopify/react-native-skia' -import { memo, useMemo } from 'react' +import React, { memo, useMemo } from 'react' import 'react-native-reanimated' -import { Flex, flexStyles } from 'ui/src/components/layout' -import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' import { + blurs, UniconAttributeData, UniconAttributes, UniconAttributesToIndices, - blurs, -} from './types.native' -import { deriveUniconAttributeIndices, getUniconAttributeData, isEthAddress } from './utils.native' +} from 'src/components/unicons/types' +import { + deriveUniconAttributeIndices, + getUniconAttributeData, + isEthAddress, +} from 'src/components/unicons/utils' +import { Flex, flexStyles } from 'ui/src' +import { useSelectedColorScheme } from 'wallet/src/features/appearance/hooks' // HACK: Add 1 to effectively increase margin between svg and surrounding box, otherwise get a cropping issue const ORIGINAL_SVG_SIZE = 36 + 1 @@ -251,7 +255,7 @@ export function _Unicon({ showBorder = false, }: Props): JSX.Element | null { // TODO(MOB-75): move this into a mandatory boolean prop for the Unicon component (e.g. `lightModeOverlay`) so that any consumer of the Unicon component has to decide whether or not to show the light mode overlay (presumably based on whether the current theme is light or dark) - const isLightMode = !useIsDarkMode() + const isLightMode = useSelectedColorScheme() === 'light' // Renders a Unicon inside a (size) x (size) pixel square Box const attributeIndices = useMemo( diff --git a/apps/mobile/src/components/unicons/__snapshots__/Unicon.test.tsx.snap b/apps/mobile/src/components/unicons/__snapshots__/Unicon.test.tsx.snap index 3b520e6d3a9..d7bc0150492 100644 --- a/apps/mobile/src/components/unicons/__snapshots__/Unicon.test.tsx.snap +++ b/apps/mobile/src/components/unicons/__snapshots__/Unicon.test.tsx.snap @@ -6,6 +6,7 @@ exports[`renders a Unicon 1`] = ` void -}): JSX.Element { - const { t } = useTranslation() - const navigation = useNavigation() - const dispatch = useAppDispatch() - const { data: deviceId } = useAsyncData(getUniqueId) - const [changeUnitagMutation] = useUnitagChangeMutation() - const [newUnitag, setNewUnitag] = useState(unitag) - const [keyboardHeight, setKeyboardHeight] = useState(0) - const debouncedInputValue = useDebounce(newUnitag, ONE_SECOND_MS) - const { error, loading } = useCanClaimUnitagName(address, debouncedInputValue) - const { triggerRefetchUnitags } = useUnitagUpdater() - - const isUnitagEdited = unitag !== newUnitag - const isUnitagValid = isUnitagEdited && !error && !loading && !!newUnitag - - const onFinishEditing = (): void => { - Keyboard.dismiss() - } - - const onChangeSubmit = async (): Promise => { - if (!deviceId) { - logger.error(new Error('DeviceId is undefined'), { - tags: { file: 'ChangeUnitagModal', function: 'onChangeSubmit' }, - }) - return // Should never hit this condition. Button is disabled if deviceId is undefined - } - - onFinishEditing() - try { - // Change unitag backend call - const { data: changeResponse } = await changeUnitagMutation({ - username: newUnitag, - address, - deviceId, - }) - - // If change failed and returns an error code, display the error message - if (!changeResponse?.data.success && !!changeResponse?.data.errorCode) { - dispatch( - pushNotification({ - type: AppNotificationType.Error, - errorMessage: parseUnitagErrorCode(t, newUnitag, changeResponse?.data.errorCode), - }) - ) - return - } - - // If change succeeded, exit the modal and display a success message - if (changeResponse?.data.success) { - triggerRefetchUnitags() - dispatch( - pushNotification({ - type: AppNotificationType.Success, - title: t('Username changed'), - }) - ) - navigation.goBack() - onClose() - } - } catch (e) { - // If some other error occurs, display a generic error message - dispatch( - pushNotification({ - type: AppNotificationType.Error, - errorMessage: t('Could not change username. Try again later.'), - }) - ) - onClose() - } - } - - // This useEffect makes KeyboardAvoidingView work when inside a BottomSheetModal - // Dynamically add bottom padding equal to keyboard height so that elements have room to shift up - useEffect(() => { - let showSubscription: EmitterSubscription - let hideSubscription: EmitterSubscription - - if (isIOS) { - // Using keyboardWillShow makes it feel more responsive, but only available on iOS - showSubscription = Keyboard.addListener('keyboardWillShow', (e) => { - setKeyboardHeight(e.endCoordinates.height) - }) - hideSubscription = Keyboard.addListener('keyboardWillHide', () => { - setKeyboardHeight(0) - }) - } else { - // keyboardDidShow only emits after the keyboard has fully appeared - showSubscription = Keyboard.addListener('keyboardDidShow', (e) => { - setKeyboardHeight(e.endCoordinates.height) - }) - hideSubscription = Keyboard.addListener('keyboardDidHide', () => { - setKeyboardHeight(0) - }) - } - - return () => { - showSubscription.remove() - hideSubscription.remove() - } - }, []) - - return ( - - - 0 ? keyboardHeight : '$spacing12'} - pt="$spacing12" - px="$spacing24"> - - {t('Edit username')} - - - - - - {UNITAG_SUFFIX} - - - - {isUnitagEdited && !loading && error && ( - - - {error} - - - )} - - - {t( - 'You only get two username changes per address. Once you change your username, you can’t change it back.' - )} - - - - - - - - ) -} - -const styles = StyleSheet.create({ - base: { - flex: 1, - justifyContent: 'flex-end', - }, - expand: { - flexGrow: 1, - }, -}) diff --git a/apps/mobile/src/components/unitags/ChooseNftModal.tsx b/apps/mobile/src/components/unitags/ChooseNftModal.tsx index 2949a660781..06ce2ba9760 100644 --- a/apps/mobile/src/components/unitags/ChooseNftModal.tsx +++ b/apps/mobile/src/components/unitags/ChooseNftModal.tsx @@ -1,9 +1,9 @@ +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { NftView } from 'src/components/NFT/NftView' +import { ModalName } from 'src/features/telemetry/constants' import { Flex, useSporeColors } from 'ui/src' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { NftsList } from 'wallet/src/components/nfts/NftsList' import { NFTItem } from 'wallet/src/features/nfts/types' -import { ModalName } from 'wallet/src/telemetry/constants' type ChooseNftProps = { address: string diff --git a/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx b/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx index fd6bb7152cc..a8b85da09a3 100644 --- a/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx +++ b/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { ImageLibraryOptions, launchImageLibrary } from 'react-native-image-picker' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' import { ChooseNftModal } from 'src/components/unitags/ChooseNftModal' +import { ElementName, ModalName } from 'src/features/telemetry/constants' import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' // Selected image will be shrunk to max width/height // URI will then be for an image of those dimensions diff --git a/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx b/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx deleted file mode 100644 index 612fbaa0b98..00000000000 --- a/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useNavigation } from '@react-navigation/native' -import { useTranslation } from 'react-i18next' -import { Button, Flex, Icons, Text } from 'ui/src' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' -import { pushNotification } from 'wallet/src/features/notifications/slice' -import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { deleteUnitag } from 'wallet/src/features/unitags/api' -import { useUnitagUpdater } from 'wallet/src/features/unitags/context' -import { useAppDispatch } from 'wallet/src/state' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' - -export function DeleteUnitagModal({ - unitag, - address, - onClose, -}: { - unitag: string - address: Address - onClose: () => void -}): JSX.Element { - const { t } = useTranslation() - const navigation = useNavigation() - const dispatch = useAppDispatch() - const { triggerRefetchUnitags } = useUnitagUpdater() - - const handleDeleteError = (): void => { - dispatch( - pushNotification({ - type: AppNotificationType.Error, - errorMessage: t('Could not delete username. Try again later.'), - }) - ) - onClose() - } - - const onDelete = async (): Promise => { - try { - const { data: deleteResponse } = await deleteUnitag(unitag, address) - if (!deleteResponse?.success) { - handleDeleteError() - return - } - - if (deleteResponse?.success) { - triggerRefetchUnitags() - dispatch( - pushNotification({ - type: AppNotificationType.Success, - title: t('Username deleted'), - }) - ) - navigation.goBack() - onClose() - } - } catch (e) { - handleDeleteError() - } - } - - return ( - - - - - - - {t('Are you sure?')} - - - {t( - 'You’re about to delete your username and customizable profile details. You will not be able to reclaim it.' - )} - - - - - - - ) -} diff --git a/apps/mobile/src/components/unitags/HeaderRow.tsx b/apps/mobile/src/components/unitags/ScreenRow.tsx similarity index 96% rename from apps/mobile/src/components/unitags/HeaderRow.tsx rename to apps/mobile/src/components/unitags/ScreenRow.tsx index 0cd097ffe36..18c57e3c816 100644 --- a/apps/mobile/src/components/unitags/HeaderRow.tsx +++ b/apps/mobile/src/components/unitags/ScreenRow.tsx @@ -3,7 +3,7 @@ import { BackButton } from 'src/components/buttons/BackButton' import { Flex, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' -export function HeaderRow({ +export function ScreenRow({ headingText, tooltipButton, }: { diff --git a/apps/mobile/src/components/unitags/UnitagBanner.tsx b/apps/mobile/src/components/unitags/UnitagBanner.tsx index fe21fca0e1d..cfcefe2cab2 100644 --- a/apps/mobile/src/components/unitags/UnitagBanner.tsx +++ b/apps/mobile/src/components/unitags/UnitagBanner.tsx @@ -2,22 +2,21 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { useAppDispatch } from 'src/app/hooks' import { openModal } from 'src/features/modals/modalSlice' -import { Button, Flex, Image, Text, useDeviceDimensions, useIsDarkMode } from 'ui/src' -import { UNITAGS_BANNER_VERTICAL_DARK, UNITAGS_BANNER_VERTICAL_LIGHT } from 'ui/src/assets' -import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { markAccountDismissedUnitagPrompt } from 'wallet/src/features/wallet/slice' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' +import { ElementName, ModalName } from 'src/features/telemetry/constants' +import { Button, Flex, Image, Text, useDeviceDimensions } from 'ui/src' +import { UNITAGS_BANNER_VERTICAL } from 'ui/src/assets' -const IMAGE_ASPECT_RATIO = 0.4 +const IMAGE_ASPECT_RATIO = 0.35 const IMAGE_SCREEN_WIDTH_PROPORTION = 0.2 const COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION = 0.16 +const COMPACT_IMAGE_TOP_SHIFT = -0.17 +const REGULAR_IMAGE_TOP_SHIFT = -0.12 +const SHORT_IMAGE_TOP_SHIFT = -0.07 export function UnitagBanner({ compact }: { compact?: boolean }): JSX.Element { const dispatch = useAppDispatch() const { t } = useTranslation() const { fullWidth } = useDeviceDimensions() - const isDarkMode = useIsDarkMode() - const activeAddress = useActiveAccountAddressWithThrow() const imageWidth = compact ? COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth : IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth @@ -28,7 +27,7 @@ export function UnitagBanner({ compact }: { compact?: boolean }): JSX.Element { } const onPressMaybeLater = (): void => { - dispatch(markAccountDismissedUnitagPrompt(activeAddress)) + // TODO (MOB-1554): set a flag in redux to not show this again } return ( @@ -66,10 +65,12 @@ export function UnitagBanner({ compact }: { compact?: boolean }): JSX.Element { {t('Claim your Uniswap username')} - {t('Sharing your address and connecting with friends has never been easier.')} + {t( + 'Get a free username and personalized profile so people can find your across web3' + )} - + + - - - ) } -function BodyItem({ Icon, title }: { Icon: GeneratedIcon; title: string }): JSX.Element { +function BodyItem({ + Icon, + title, + subtitle, +}: { + Icon: GeneratedIcon + title: string + subtitle: string +}): JSX.Element { return ( - - - - {title} - + + + + + {title} + + + {subtitle} + + ) } diff --git a/apps/mobile/src/components/unitags/WalletSelectorModal.tsx b/apps/mobile/src/components/unitags/WalletSelectorModal.tsx new file mode 100644 index 00000000000..1d462bc8a5d --- /dev/null +++ b/apps/mobile/src/components/unitags/WalletSelectorModal.tsx @@ -0,0 +1,159 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' +import { Unicon } from 'src/components/unicons/Unicon' +import { ElementName, ModalName } from 'src/features/telemetry/constants' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { Button, Flex, Icons, Separator, Text, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { createAccountActions } from 'wallet/src/features/wallet/create/createAccountSaga' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { + useDisplayName, + useNativeAccountExists, + useSignerAccounts, +} from 'wallet/src/features/wallet/hooks' +import { shortenAddress } from 'wallet/src/utils/addresses' +type WalletSelectorModalProps = { + activeAccount: Account | null + onPressAccount: (account: Account) => void + onClose: () => void +} + +export const WalletSelectorModal = ({ + activeAccount, + onPressAccount, + onClose, +}: WalletSelectorModalProps): JSX.Element => { + const colors = useSporeColors() + const { t } = useTranslation() + const signerAccounts = useSignerAccounts() + const dispatch = useAppDispatch() + const hasImportedSeedPhrase = useNativeAccountExists() + + const options = signerAccounts.map((account) => { + return { + key: `${ElementName.AccountCard}-${account.address}`, + onPress: () => onPressAccount(account), + render: () => , + } + }) + + const onPressNewWallet = (): void => { + // Clear any existing pending accounts first. + dispatch(pendingAccountActions.trigger(PendingAccountActions.Delete)) + dispatch(createAccountActions.trigger()) + + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.EditName, + params: { + importType: hasImportedSeedPhrase ? ImportType.CreateAdditional : ImportType.CreateNew, + entryPoint: OnboardingEntryPoint.Sidebar, + }, + }) + onClose() + } + + return ( + + + + {t('Choose a wallet to map to')} + + {t( + 'Choose which wallet you want to assign your username to. You can only claim on 1 wallet, so choose wisely.' + )} + + + + {options.map((option) => ( + + {option.render()} + + ))} + + + + {t('or')} + + + + + + + + + + + + + ) +} + +export const SwitchAccountOption = ({ + account, + activeAccount, +}: { + account: Account + activeAccount: Account | null +}): JSX.Element => { + const displayName = useDisplayName(account.address) + return ( + + + + + {displayName?.name} + + + {shortenAddress(account.address)} + + + + ) +} diff --git a/apps/mobile/src/data/cache.ts b/apps/mobile/src/data/cache.ts index 64855c07ab7..a06a194e961 100644 --- a/apps/mobile/src/data/cache.ts +++ b/apps/mobile/src/data/cache.ts @@ -1,7 +1,7 @@ import { InMemoryCache } from '@apollo/client' import { MMKVWrapper, persistCache } from 'apollo3-cache-persist' import { logger } from 'utilities/src/logger/logger' -import { setupWalletCache } from 'wallet/src/data/cache' +import { setupCache } from 'wallet/src/data/cache' const MAX_CACHE_SIZE_IN_BYTES = 1024 * 1024 * 25 // 25 MB @@ -11,7 +11,7 @@ const MAX_CACHE_SIZE_IN_BYTES = 1024 * 1024 * 25 // 25 MB * @returns */ export async function initAndPersistCache(storage: MMKVWrapper): Promise { - const cache = setupWalletCache() + const cache = setupCache() try { await persistCache({ diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx b/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx index 8fcaa1996bc..879c93b845c 100644 --- a/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx +++ b/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx @@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next' import { Keyboard, TextInput } from 'react-native' import { PasswordInput } from 'src/components/input/PasswordInput' import { PasswordError } from 'src/features/onboarding/PasswordError' -import { Button, Flex, Icons, Text } from 'ui/src' -import { iconSizes } from 'ui/src/theme' -import { ElementName } from 'wallet/src/telemetry/constants' +import { ElementName } from 'src/features/telemetry/constants' +import { Button, CheckBox, Flex } from 'ui/src' import { validatePassword } from 'wallet/src/utils/password' export enum PasswordErrors { @@ -29,9 +28,10 @@ export function CloudBackupPasswordForm({ const passwordInputRef = useRef(null) const [password, setPassword] = useState('') + const [consentChecked, setConsentChecked] = useState(false) const [error, setError] = useState(undefined) - const isButtonDisabled = !!error || password.length === 0 + const isButtonDisabled = (!isConfirmation && !consentChecked) || !!error || password.length === 0 const onPasswordChangeText = (newPassword: string): void => { if (isConfirmation && newPassword === password) { @@ -44,6 +44,10 @@ export function CloudBackupPasswordForm({ setPassword(newPassword) } + const onPressConsent = (): void => { + setConsentChecked(!consentChecked) + } + const onPasswordSubmitEditing = (): void => { const { valid, validationErrorString } = validatePassword(password) if (!isConfirmation && !valid) { @@ -102,14 +106,13 @@ export function CloudBackupPasswordForm({ {error ? : null} {!isConfirmation && ( - - - - {t( - 'Uniswap Labs does not store your password and can’t recover it, so it’s crucial you remember it.' - )} - - + )} - - - ) - } - - if (OTP) { - return ( - - - - - - - Uniswap one-time code - - - - Enter this code in the Uniswap Extension. Your recovery phrase will be safely - encrypted and transferred. - - - - {OTP.substring(0, 3)} {OTP.substring(3)} - - - {expiryText} - - - - ) - } - return ( - - - - - - - Is this your device? - - - - Only continue if you are syncing with the Uniswap Extension on a trusted device. - - - {device && ( - - - Device - - {device} - - )} - {browser && ( - - - Browser - - {browser} - - )} - - - - - - - ) -} diff --git a/apps/mobile/src/features/scantastic/ScantasticModalState.ts b/apps/mobile/src/features/scantastic/ScantasticModalState.ts deleted file mode 100644 index 59bdd6d364f..00000000000 --- a/apps/mobile/src/features/scantastic/ScantasticModalState.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ScantasticModalState { - uuid: string - pubKey: string - vendor: string - model: string - browser: string -} diff --git a/apps/mobile/src/features/send/hooks.ts b/apps/mobile/src/features/send/hooks.ts index ba23e597e6e..7cb84e5185a 100644 --- a/apps/mobile/src/features/send/hooks.ts +++ b/apps/mobile/src/features/send/hooks.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { ChainId } from 'wallet/src/constants/chains' import { AssetType } from 'wallet/src/entities/assets' import { @@ -7,7 +8,6 @@ import { TransactionState, } from 'wallet/src/features/transactions/transactionState/types' import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' export const useNavigateToSend: () => ( currencyAddress: Address, diff --git a/apps/mobile/src/features/swap/hooks.ts b/apps/mobile/src/features/swap/hooks.ts index a988f1e5533..77e898bf55e 100644 --- a/apps/mobile/src/features/swap/hooks.ts +++ b/apps/mobile/src/features/swap/hooks.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' import { openModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'src/features/telemetry/constants' import { getNativeAddress } from 'wallet/src/constants/addresses' import { ChainId } from 'wallet/src/constants/chains' import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' @@ -8,7 +9,6 @@ import { TransactionState, } from 'wallet/src/features/transactions/transactionState/types' import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' import { areAddressesEqual } from 'wallet/src/utils/addresses' export const useNavigateToSwap: () => ( diff --git a/apps/mobile/src/features/telemetry/constants.ts b/apps/mobile/src/features/telemetry/constants.ts index bc2f67d19f2..b8dab32172a 100644 --- a/apps/mobile/src/features/telemetry/constants.ts +++ b/apps/mobile/src/features/telemetry/constants.ts @@ -48,18 +48,21 @@ export enum MobileEventName { DeepLinkOpened = 'Deep Link Opened', ExploreFilterSelected = 'Explore Filter Selected', ExploreSearchResultClicked = 'Explore Search Result Clicked', + ExploreSearchCancel = 'Explore Search Cancel', ExploreTokenItemSelected = 'Explore Token Item Selected', FavoriteItem = 'Favorite Item', FiatOnRampBannerPressed = 'Fiat OnRamp Banner Pressed', FiatOnRampQuickActionButtonPressed = 'Fiat OnRamp QuickAction Button Pressed', FiatOnRampAmountEntered = 'Fiat OnRamp Amount Entered', FiatOnRampWidgetOpened = 'Fiat OnRamp Widget Opened', + NetworkFilterSelected = 'Network Filter Selected', OnboardingCompleted = 'Onboarding Completed', PerformanceReport = 'Performance Report', PerformanceGraphql = 'Performance GraphQL', ShareButtonClicked = 'Share Button Clicked', ShareLinkOpened = 'Share Link Opened', TokenDetailsOtherChainButtonPressed = 'Token Details Other Chain Button Pressed', + TokenSelected = 'Token Selected', WalletAdded = 'Wallet Added', WalletConnectSheetCompleted = 'Wallet Connect Sheet Completed', WidgetConfigurationUpdated = 'Widget Configuration Updated', @@ -67,6 +70,85 @@ export enum MobileEventName { // alphabetize additional values. } +/** + * Possible names for the section property in TraceContext + */ +export const enum SectionName { + CurrencyInputPanel = 'currency-input-panel', + CurrencyOutputPanel = 'currency-output-panel', + ExploreFavoriteTokensSection = 'explore-favorite-tokens-section', + ExploreSearch = 'explore-search', + ExploreTopTokensSection = 'explore-top-tokens-section', + HomeActivityTab = 'home-activity-tab', + HomeFeedTab = 'home-feed-tab', + HomeNFTsTab = 'home-nfts-tab', + HomeTokensTab = 'home-tokens-tab', + ImportAccountForm = 'import-account-form', + ProfileActivityTab = 'profile-activity-tab', + ProfileNftsTab = 'profile-nfts-tab', + ProfileTokensTab = 'profile-tokens-tab', + SwapForm = 'swap-form', + SwapPending = 'swap-pending', + SwapReview = 'swap-review', + TokenSelector = 'token-selector', + TokenDetails = 'token-details', + TransferForm = 'transfer-form', + TransferPending = 'transfer-pending', + TransferReview = 'transfer-review', + // alphabetize additional values. +} + +/** + * Possible names for the modal property in TraceContext + */ +export const enum ModalName { + AccountEdit = 'account-edit-modal', + AccountSwitcher = 'account-switcher-modal', + AddWallet = 'add-wallet-modal', + BlockedAddress = 'blocked-address', + ChooseProfilePhoto = 'choose-profile-photo-modal', + CloudBackupInfo = 'cloud-backup-info-modal', + Experiments = 'experiments', + Explore = 'explore-modal', + FaceIDWarning = 'face-id-warning', + FOTInfo = 'fee-on-transfer', + FiatCurrencySelector = 'fiat-currency-selector', + FiatOnRamp = 'fiat-on-ramp', + FiatOnRampAggregator = 'fiat-on-ramp-aggregator', + FiatOnRampCountryList = 'fiat-on-ramp-country-list', + ForceUpgradeModal = 'force-upgrade-modal', + LanguageSelector = 'language-selector-modal', + NetworkFeeInfo = 'network-fee-info', + NetworkSelector = 'network-selector-modal', + NftCollection = 'nft-collection', + QRCodeNetworkInfo = 'qr-code-network-info', + RemoveWallet = 'remove-wallet-modal', + RestoreWallet = 'restore-wallet-modal', + RemoveSeedPhraseWarningModal = 'remove-seed-phrase-warning-modal', + ScreenshotWarning = 'screenshot-warning', + Send = 'send-modal', + SeedPhraseWarningModal = 'seed-phrase-warning-modal', + SendWarning = 'send-warning-modal', + SlippageInfo = 'slippage-info-modal', + Swap = 'swap-modal', + SwapSettings = 'swap-settings-modal', + SwapWarning = 'swap-warning-modal', + SwapProtection = 'swap-protection-modal', + TokenSelector = 'token-selector', + TokenWarningModal = 'token-warning-modal', + TooltipContent = 'tooltip-content', + TransactionActions = 'transaction-actions', + UnitagsIntro = 'unitags-intro-modal', + ViewSeedPhraseWarning = 'view-seed-phrase-warning', + ViewOnlyExplainer = 'view-only-explainer-modal', + WalletConnectScan = 'wallet-connect-scan-modal', + WCDappConnectedNetworks = 'wc-dapp-connected-networks-modal', + WCPendingConnection = 'wc-pending-connection-modal', + WCSignRequest = 'wc-sign-request-modal', + WCViewOnlyWarning = 'wc-view-only-warning-modal', + // alphabetize additional values. +} + /** * Views not within the navigation stack that we still want to * log Pageview events for. (Usually presented as nested views within another screen) @@ -76,6 +158,94 @@ export const enum ManualPageViewScreen { ConfirmRecoveryPhrase = 'ConfirmRecoveryPhrase', } +/** + * Possible names for the element property in TraceContext + */ +export const enum ElementName { + AcceptNewRate = 'accept-new-rate', + AccountCard = 'account-card', + AddManualBackup = 'add-manual-backup', + AddViewOnlyWallet = 'add-view-only-wallet', + AddCloudBackup = 'add-cloud-backup', + Back = 'back', + Buy = 'buy', + Cancel = 'cancel', + Confirm = 'confirm', + Continue = 'continue', + Copy = 'copy', + CreateAccount = 'create-account', + Edit = 'edit', + EmptyStateBuy = 'empty-state-buy', + EmptyStateGetStarted = 'empty-state-get-started', + EmptyStateImport = 'empty-state-get-import', + EmptyStateReceive = 'empty-state-receive', + Enable = 'enable', + EtherscanView = 'etherscan-view', + Favorite = 'favorite', + FiatOnRampTokenSelector = 'fiat-on-ramp-token-selector', + FiatOnRampAggregatorTokenSelector = 'fiat-on-ramp-aggregator-token-selector', + FiatOnRampWidgetButton = 'fiat-on-ramp-widget-button', + FiatOnRampCountryPicker = 'fiat-on-ramp-country-picker', + GetHelp = 'get-help', + GetStarted = 'get-started', + ImportAccount = 'import', + Manage = 'manage', + MoonpayExplorerView = 'moonpay-explorer-view', + NetworkButton = 'network-button', + Next = 'next', + OK = 'ok', + OnboardingImportBackup = 'onboarding-import-backup', + OnboardingImportSeedPhrase = 'onboarding-import-seed-phrase', + OnboardingImportWatchedAccount = 'onboarding-import-watched-account', + OpenDeviceLanguageSettings = 'open-device-language-settings', + OpenCameraRoll = 'open-camera-roll', + OpenNftsList = 'open-nfts-list', + QRCodeModalToggle = 'qr-code-modal-toggle', + Receive = 'receive', + RecoveryHelpButton = 'recovery-help-button', + Remove = 'remove', + RestoreFromCloud = 'restore-from-cloud', + RestoreWallet = 'restore-wallet', + ReviewSwap = 'review-swap', + ReviewTransfer = 'review-transfer', + SearchEtherscanItem = 'search-etherscan-item', + SearchNFTCollectionItem = 'search-nft-collection-item', + SelectRecipient = 'select-recipient', + SearchTokenItem = 'search-token-item', + Sell = 'sell', + Send = 'send', + SetMaxInput = 'set-max-input', + SetMaxOutput = 'set-max-output', + Skip = 'skip', + Submit = 'submit', + Swap = 'swap', + SwapReview = 'swap-review', + SwapSettings = 'swap-settings', + SwitchCurrenciesButton = 'switch-currencies-button', + TimeFrame1H = 'time-frame-1H', + TimeFrame1D = 'time-frame-1D', + TimeFrame1W = 'time-frame-1W', + TimeFrame1M = 'time-frame-1M', + TimeFrame1Y = 'time-frame-1Y', + TokenAddress = 'token-address', + TokenInputSelector = 'token-input-selector', + TokenLinkEtherscan = 'token-link-etherscan', + TokenLinkTwitter = 'token-link-twitter', + TokenLinkWebsite = 'token-link-website', + TokenOutputSelector = 'token-output-selector', + TokenSelectorToggle = 'token-selector-toggle', + TokenWarningAccept = 'token-warning-accept', + Unwrap = 'unwrap', + WCDappSwitchAccount = 'wc-dapp-switch-account', + WCDappNetworks = 'wc-dapp-networks', + WalletCard = 'wallet-card', + WalletConnectScan = 'wallet-connect-scan', + WalletQRCode = 'wallet-qr-code', + WalletSettings = 'WalletSettings', + Wrap = 'wrap', + // alphabetize additional values. +} + /** * User properties tied to user rather than events */ diff --git a/apps/mobile/src/features/telemetry/hooks.ts b/apps/mobile/src/features/telemetry/hooks.ts index 451d4c6e260..eff095502f4 100644 --- a/apps/mobile/src/features/telemetry/hooks.ts +++ b/apps/mobile/src/features/telemetry/hooks.ts @@ -1,5 +1,6 @@ import { useCallback, useMemo } from 'react' import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useAccountList } from 'src/components/accounts/hooks' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { MobileEventName } from 'src/features/telemetry/constants' import { @@ -17,7 +18,6 @@ import { } from 'src/features/telemetry/slice' import { useAsyncData } from 'utilities/src/react/hooks' import { areSameDays } from 'utilities/src/time/date' -import { useAccountList } from 'wallet/src/features/accounts/hooks' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' import { sendWalletAppsFlyerEvent } from 'wallet/src/telemetry' diff --git a/apps/mobile/src/features/telemetry/slice.ts b/apps/mobile/src/features/telemetry/slice.ts index fd9a6f8abe6..5ee812ca93a 100644 --- a/apps/mobile/src/features/telemetry/slice.ts +++ b/apps/mobile/src/features/telemetry/slice.ts @@ -47,8 +47,7 @@ export const slice = createSlice({ setAllowAnalytics: (state, { payload: { enabled } }: PayloadAction<{ enabled: boolean }>) => { sendWalletAnalyticsEvent(SharedEventName.ANALYTICS_SWITCH_TOGGLED, { enabled }) analytics.flushEvents() - // eslint-disable-next-line no-void - void analytics.setAllowAnalytics(enabled).finally(() => undefined) + analytics.setAllowAnalytics(enabled).finally(() => undefined) state.allowAnalytics = enabled }, }, diff --git a/apps/mobile/src/features/telemetry/timing/selectors.ts b/apps/mobile/src/features/telemetry/timing/selectors.ts new file mode 100644 index 00000000000..3f5f3789ddc --- /dev/null +++ b/apps/mobile/src/features/telemetry/timing/selectors.ts @@ -0,0 +1,4 @@ +import { MobileState } from 'src/app/reducer' + +export const selectSwapStartTimestamp = (state: MobileState): number | undefined => + state.timing.swap.startTimestamp diff --git a/packages/wallet/src/telemetry/timing/slice.ts b/apps/mobile/src/features/telemetry/timing/slice.ts similarity index 100% rename from packages/wallet/src/telemetry/timing/slice.ts rename to apps/mobile/src/features/telemetry/timing/slice.ts diff --git a/apps/mobile/src/features/telemetry/types.ts b/apps/mobile/src/features/telemetry/types.ts index 372c9c38fb8..5deb81c6b44 100644 --- a/apps/mobile/src/features/telemetry/types.ts +++ b/apps/mobile/src/features/telemetry/types.ts @@ -1,10 +1,17 @@ +import { ApolloError } from '@apollo/client' +import { SerializedError } from '@reduxjs/toolkit' +import { FetchBaseQueryError } from '@reduxjs/toolkit/dist/query/fetchBaseQuery' import { RenderPassReport } from '@shopify/react-native-performance' -import { SharedEventName } from '@uniswap/analytics-events' +import { SharedEventName, SwapEventName } from '@uniswap/analytics-events' +import { providers } from 'ethers' import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' import { WidgetEvent, WidgetType } from 'src/features/widgets/widgets' import { TraceProps } from 'utilities/src/telemetry/trace/Trace' +import { ChainId } from 'wallet/src/constants/chains' import { ImportType } from 'wallet/src/features/onboarding/types' +import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { EthMethod, WCEventType, WCRequestOutcome } from 'wallet/src/features/walletConnect/types' +import { SwapTradeBaseProperties } from 'wallet/src/telemetry/types' // Events related to Moonpay internal transactions // NOTE: we do not currently have access to the full life cycle of these txs @@ -57,6 +64,9 @@ export type MobileEventProperties = { AssetDetailsBaseProperties & { type: 'collection' | 'token' | 'address' } + [MobileEventName.ExploreSearchCancel]: { + query: string + } [MobileEventName.ExploreTokenItemSelected]: AssetDetailsBaseProperties & { position: number } @@ -67,6 +77,9 @@ export type MobileEventProperties = { [MobileEventName.FiatOnRampBannerPressed]: TraceProps [MobileEventName.FiatOnRampAmountEntered]: TraceProps & { source: 'chip' | 'textInput' } [MobileEventName.FiatOnRampWidgetOpened]: TraceProps & { externalTransactionId: string } + [MobileEventName.NetworkFilterSelected]: TraceProps & { + chain: ChainId | 'All' + } [MobileEventName.OnboardingCompleted]: OnboardingCompletedProps & TraceProps [MobileEventName.PerformanceReport]: RenderPassReport [MobileEventName.PerformanceGraphql]: { @@ -84,6 +97,11 @@ export type MobileEventProperties = { url: string } [MobileEventName.TokenDetailsOtherChainButtonPressed]: TraceProps + [MobileEventName.TokenSelected]: TraceProps & + AssetDetailsBaseProperties & + SearchResultContextProperties & { + field: CurrencyField + } [MobileEventName.WalletAdded]: OnboardingCompletedProps & TraceProps [MobileEventName.WalletConnectSheetCompleted]: { request_type: WCEventType @@ -103,4 +121,24 @@ export type MobileEventProperties = { [SharedEventName.APP_LOADED]: TraceProps | undefined [SharedEventName.ELEMENT_CLICKED]: TraceProps [SharedEventName.PAGE_VIEWED]: TraceProps + [SwapEventName.SWAP_DETAILS_EXPANDED]: TraceProps | undefined + [SwapEventName.SWAP_QUOTE_RECEIVED]: { + quote_latency_milliseconds?: number + } & SwapTradeBaseProperties + [SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED]: { + estimated_network_fee_wei?: string + gas_limit?: string + transaction_deadline_seconds?: number + token_in_amount_usd?: number + token_out_amount_usd?: number + is_auto_slippage?: boolean + swap_quote_block_number?: string + swap_flow_duration_milliseconds?: number + is_hold_to_swap?: boolean + is_fiat_input_mode?: boolean + } & SwapTradeBaseProperties + [SwapEventName.SWAP_ESTIMATE_GAS_CALL_FAILED]: { + error?: ApolloError | FetchBaseQueryError | SerializedError | Error | string + txRequest?: providers.TransactionRequest + } & SwapTradeBaseProperties } diff --git a/packages/wallet/src/features/tokens/dismissedWarningTokensSelector.ts b/apps/mobile/src/features/tokens/dismissedWarningTokensSelector.ts similarity index 63% rename from packages/wallet/src/features/tokens/dismissedWarningTokensSelector.ts rename to apps/mobile/src/features/tokens/dismissedWarningTokensSelector.ts index 72d0ad2368c..ae7b458f105 100644 --- a/packages/wallet/src/features/tokens/dismissedWarningTokensSelector.ts +++ b/apps/mobile/src/features/tokens/dismissedWarningTokensSelector.ts @@ -1,9 +1,9 @@ -import type { SharedState } from 'wallet/src/state/reducer' +import type { MobileState } from 'src/app/reducer' // selectors export const dismissedWarningTokensSelector = ( - state: SharedState + state: MobileState ): { [currencyId: string]: boolean } => state.tokens.dismissedWarningTokens diff --git a/packages/wallet/src/features/tokens/hooks.ts b/apps/mobile/src/features/tokens/hooks.ts similarity index 69% rename from packages/wallet/src/features/tokens/hooks.ts rename to apps/mobile/src/features/tokens/hooks.ts index 97aadbaa15a..03a8e4e1f52 100644 --- a/packages/wallet/src/features/tokens/hooks.ts +++ b/apps/mobile/src/features/tokens/hooks.ts @@ -1,4 +1,6 @@ -import { useMemo } from 'react' +import { impactAsync } from 'expo-haptics' +import { useCallback, useMemo } from 'react' +import { setClipboard } from 'src/utils/clipboard' import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' import { ChainId } from 'wallet/src/constants/chains' import { @@ -6,6 +8,9 @@ import { SearchPopularTokensQuery, useSearchPopularTokensQuery, } from 'wallet/src/data/__generated__/types-and-hooks' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { useAppDispatch } from 'wallet/src/state' import { areAddressesEqual } from 'wallet/src/utils/addresses' export type TopToken = NonNullable[0]> @@ -50,3 +55,15 @@ export function usePopularTokens(): { return { popularTokens, loading } } + +export function useCopyTokenAddressCallback(tokenAddress: Address): () => void { + const dispatch = useAppDispatch() + return useCallback(async () => { + await impactAsync() + await setClipboard(tokenAddress) + + dispatch( + pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address }) + ) + }, [tokenAddress, dispatch]) +} diff --git a/packages/wallet/src/features/tokens/safetyHooks.ts b/apps/mobile/src/features/tokens/safetyHooks.ts similarity index 82% rename from packages/wallet/src/features/tokens/safetyHooks.ts rename to apps/mobile/src/features/tokens/safetyHooks.ts index cd228708456..1a94e3308fb 100644 --- a/packages/wallet/src/features/tokens/safetyHooks.ts +++ b/apps/mobile/src/features/tokens/safetyHooks.ts @@ -1,10 +1,10 @@ import { useCallback } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { addDismissedWarningToken } from 'src/features/tokens/tokensSlice' import { ThemeKeys } from 'ui/src' import { SafetyLevel } from 'wallet/src/data/__generated__/types-and-hooks' -import { dismissedWarningTokensSelector } from 'wallet/src/features/tokens/dismissedWarningTokensSelector' -import { addDismissedWarningToken } from 'wallet/src/features/tokens/tokensSlice' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' import { CurrencyId } from 'wallet/src/utils/currencyId' +import { dismissedWarningTokensSelector } from './dismissedWarningTokensSelector' export function useTokenWarningDismissed(currencyId: Maybe): { tokenWarningDismissed: boolean // user dismissed warning diff --git a/packages/wallet/src/features/tokens/tokensSlice.ts b/apps/mobile/src/features/tokens/tokensSlice.ts similarity index 100% rename from packages/wallet/src/features/tokens/tokensSlice.ts rename to apps/mobile/src/features/tokens/tokensSlice.ts diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx b/apps/mobile/src/features/transactions/SummaryCards/CancelConfirmationView.tsx similarity index 84% rename from packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx rename to apps/mobile/src/features/transactions/SummaryCards/CancelConfirmationView.tsx index 0486bed0d84..b94b4a974fd 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/CancelConfirmationView.tsx @@ -1,27 +1,26 @@ import { providers } from 'ethers' import { notificationAsync } from 'expo-haptics' -import { useCallback } from 'react' +import { default as React, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { ActivityIndicator } from 'react-native' +import { AddressDisplay } from 'src/components/AddressDisplay' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { useCancelationGasFeeInfo } from 'src/features/gas/hooks' +import { ElementName } from 'src/features/telemetry/constants' import { Button, Flex, Text, useSporeColors } from 'ui/src' import SlashCircleIcon from 'ui/src/assets/icons/slash-circle.svg' import { NumberType } from 'utilities/src/format/types' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { AuthTrigger } from 'wallet/src/features/auth/types' -import { useCancelationGasFeeInfo, useUSDValue } from 'wallet/src/features/gas/hooks' +import { useUSDValue } from 'wallet/src/features/gas/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { TransactionDetails, TransactionStatus } from 'wallet/src/features/transactions/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { ElementName } from 'wallet/src/telemetry/constants' import { shortenAddress } from 'wallet/src/utils/addresses' export function CancelConfirmationView({ - authTrigger, onBack, onCancel, transactionDetails, }: { - authTrigger?: AuthTrigger onBack: () => void onCancel: (txRequest: providers.TransactionRequest) => void transactionDetails: TransactionDetails @@ -46,14 +45,17 @@ export function CancelConfirmationView({ onCancel(cancelationGasFeeInfo.cancelRequest) }, [cancelationGasFeeInfo, onCancel]) + const { trigger: actionButtonTrigger } = useBiometricPrompt(onCancelConfirm) + const { requiredForTransactions } = useBiometricAppSettings() + const onPressCancel = useCallback(async () => { await notificationAsync() - if (authTrigger) { - await authTrigger({ successCallback: onCancelConfirm, failureCallback: () => {} }) + if (requiredForTransactions) { + await actionButtonTrigger() } else { onCancelConfirm() } - }, [onCancelConfirm, authTrigger]) + }, [onCancelConfirm, requiredForTransactions, actionButtonTrigger]) return ( JSX.Element { return function OptionItem(): JSX.Element { @@ -89,7 +86,7 @@ export default function TransactionActionsModal({ onViewTokenDetails && isSwapTransaction && inputCurrencyInfo && outputCurrencyInfo ? [ { - key: inputCurrencyInfo.currencyId, + key: ElementName.TokenAddress, onPress: () => onViewTokenDetails(inputCurrencyInfo.currencyId), render: renderOptionItem( t('View {{ tokenSymbol }}', { @@ -98,7 +95,7 @@ export default function TransactionActionsModal({ ), }, { - key: outputCurrencyInfo.currencyId, + key: ElementName.TokenAddress, onPress: () => onViewTokenDetails(outputCurrencyInfo.currencyId), render: renderOptionItem( t('View {{ tokenSymbol }}', { diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx b/apps/mobile/src/features/transactions/SummaryCards/TransactionSummaryLayout.tsx similarity index 85% rename from packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx rename to apps/mobile/src/features/transactions/SummaryCards/TransactionSummaryLayout.tsx index 4a9c03d3279..a8a1e087900 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/TransactionSummaryLayout.tsx @@ -1,37 +1,35 @@ /* eslint-disable complexity */ import { providers } from 'ethers' -import { memo, useEffect, useState } from 'react' +import { default as React, memo, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' +import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import { ModalName } from 'src/features/telemetry/constants' +import { useLowestPendingNonce } from 'src/features/transactions/hooks' +import { CancelConfirmationView } from 'src/features/transactions/SummaryCards/CancelConfirmationView' +import TransactionActionsModal from 'src/features/transactions/SummaryCards/TransactionActionsModal' +import { openMoonpayTransactionLink, openTransactionLink } from 'src/utils/linking' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import AlertTriangle from 'ui/src/assets/icons/alert-triangle.svg' import SlashCircleIcon from 'ui/src/assets/icons/slash-circle.svg' -import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' -import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' -import { CancelConfirmationView } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView' -import TransactionActionsModal from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionActionsModal' +import { cancelTransaction } from 'wallet/src/features/transactions/slice' import { TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { + getTransactionSummaryTitle, TXN_HISTORY_ICON_SIZE, TXN_STATUS_ICON_SIZE, - getTransactionSummaryTitle, useFormattedTime, } from 'wallet/src/features/transactions/SummaryCards/utils' -import { useLowestPendingNonce } from 'wallet/src/features/transactions/hooks' -import { cancelTransaction } from 'wallet/src/features/transactions/slice' import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccountWithThrow, useDisplayName } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' import { CurrencyId } from 'wallet/src/utils/currencyId' -import { openMoonpayTransactionLink, openTransactionLink } from 'wallet/src/utils/linking' const LOADING_SPINNER_SIZE = 20 function TransactionSummaryLayout({ - authTrigger, transaction, title, caption, @@ -40,8 +38,7 @@ function TransactionSummaryLayout({ }: TransactionSummaryLayoutProps): JSX.Element { const { t } = useTranslation() const colors = useSporeColors() - - const { navigateToTokenDetails } = useWalletNavigation() + const tokenDetailsNavigation = useTokenDetailsNavigation() const { type } = useActiveAccountWithThrow() const readonly = type === AccountType.Readonly @@ -140,11 +137,10 @@ function TransactionSummaryLayout({ - {walletDisplayName ? ( - + {walletDisplayName?.name ? ( + + {walletDisplayName.name} + ) : null} {title} @@ -207,7 +203,7 @@ function TransactionSummaryLayout({ ? (currencyId: CurrencyId): void | undefined => { setShowActionsModal(false) if (transaction.typeInfo.type === TransactionType.Swap) { - navigateToTokenDetails(currencyId) + tokenDetailsNavigation.navigate(currencyId) } } : undefined @@ -221,7 +217,6 @@ function TransactionSummaryLayout({ onClose={(): void => setShowCancelModal(false)}> {transaction && ( { setShowActionsModal(true) diff --git a/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx b/apps/mobile/src/features/transactions/TransactionDetails.tsx similarity index 81% rename from packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx rename to apps/mobile/src/features/transactions/TransactionDetails.tsx index 1c766d0f93f..326804de997 100644 --- a/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx +++ b/apps/mobile/src/features/transactions/TransactionDetails.tsx @@ -1,22 +1,21 @@ import { SwapEventName } from '@uniswap/analytics-events' -import { PropsWithChildren, ReactNode, useState } from 'react' +import React, { PropsWithChildren, ReactNode, useState } from 'react' import { useTranslation } from 'react-i18next' +import { AccountDetails } from 'src/components/accounts/AccountDetails' +import { Warning } from 'src/components/modals/WarningModal/types' +import { getAlertColor } from 'src/components/modals/WarningModal/WarningModal' +import { NetworkFee } from 'src/components/Network/NetworkFee' +import { OnShowSwapFeeInfo, SwapFee } from 'src/components/SwapFee/SwapFee' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { FeeOnTransferInfo } from 'src/features/transactions/swap/FeeOnTransferInfo' import { Flex, Icons, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' import AnglesMaximize from 'ui/src/assets/icons/angles-maximize.svg' import AnglesMinimize from 'ui/src/assets/icons/angles-minimize.svg' import { iconSizes } from 'ui/src/theme' -import { getAlertColor } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { NetworkFee } from 'wallet/src/components/network/NetworkFee' import { ChainId } from 'wallet/src/constants/chains' import { GasFeeResult } from 'wallet/src/features/gas/types' import { SwapFeeInfo } from 'wallet/src/features/routing/types' -import { FeeOnTransferInfo } from 'wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo' -import { - OnShowSwapFeeInfo, - SwapFee, -} from 'wallet/src/features/transactions/TransactionDetails/SwapFee' -import { Warning } from 'wallet/src/features/transactions/WarningModal/types' -import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' interface TransactionDetailsProps { banner?: ReactNode @@ -31,7 +30,6 @@ interface TransactionDetailsProps { onShowSwapFeeInfo?: OnShowSwapFeeInfo onShowWarning?: () => void isSwap?: boolean - AccountDetails?: JSX.Element } export function TransactionDetails({ @@ -48,17 +46,17 @@ export function TransactionDetails({ onShowSwapFeeInfo, onShowWarning, isSwap, - AccountDetails, }: PropsWithChildren): JSX.Element { const colors = useSporeColors() const { t } = useTranslation() + const userAddress = useActiveAccountAddressWithThrow() const warningColor = getAlertColor(warning?.severity) const [showChildren, setShowChildren] = useState(showExpandedChildren) const onPressToggleShowChildren = (): void => { if (!showChildren) { - sendWalletAnalyticsEvent(SwapEventName.SWAP_DETAILS_EXPANDED) + sendMobileAnalyticsEvent(SwapEventName.SWAP_DETAILS_EXPANDED) } setShowChildren(!showChildren) } @@ -130,7 +128,9 @@ export function TransactionDetails({ )} - {AccountDetails} + {!isSwap || !swapFeeInfo ? ( + + ) : null} ) diff --git a/apps/mobile/src/features/transactions/TransactionFlow.tsx b/apps/mobile/src/features/transactions/TransactionFlow.tsx index 2be13123ac2..5c7d369b559 100644 --- a/apps/mobile/src/features/transactions/TransactionFlow.tsx +++ b/apps/mobile/src/features/transactions/TransactionFlow.tsx @@ -2,39 +2,30 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { TouchableWithoutFeedback } from 'react-native' import { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated' -import { useShouldShowNativeKeyboard } from 'src/app/hooks' import { Screen } from 'src/components/layout/Screen' +import { useBottomSheetContext } from 'src/components/modals/BottomSheetContext' +import { HandleBar } from 'src/components/modals/HandleBar' +import { WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' import Trace from 'src/components/Trace/Trace' -import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { ModalName, SectionName } from 'src/features/telemetry/constants' +import { SwapSettingsModal } from 'src/features/transactions/swap/modals/SwapSettingsModal' import { SwapForm } from 'src/features/transactions/swap/SwapForm' import { SwapReview } from 'src/features/transactions/swap/SwapReview' import { SwapStatus } from 'src/features/transactions/swap/SwapStatus' import { HeaderContent } from 'src/features/transactions/TransactionFlowHeaderContent' +import { transactionStateActions } from 'src/features/transactions/transactionState/transactionState' +import { DerivedTransferInfo } from 'src/features/transactions/transfer/hooks' +import { TransferReview } from 'src/features/transactions/transfer/TransferReview' import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus' -import { useWalletRestore } from 'src/features/wallet/hooks' +import { TransferTokenForm } from 'src/features/transactions/transfer/TransferTokenForm' import { AnimatedFlex, Flex, useDeviceDimensions, useSporeColors } from 'ui/src' import EyeIcon from 'ui/src/assets/icons/eye.svg' import { iconSizes } from 'ui/src/theme' -import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' -import { HandleBar } from 'wallet/src/components/modals/HandleBar' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { GasFeeResult } from 'wallet/src/features/gas/types' -import { SwapSettingsModal } from 'wallet/src/features/transactions/swap/modals/SwapSettingsModal' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { transactionStateActions } from 'wallet/src/features/transactions/transactionState/transactionState' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { - useTransferERC20Callback, - useTransferNFTCallback, -} from 'wallet/src/features/transactions/transfer/hooks/useTransferCallback' -import { TransferReview } from 'wallet/src/features/transactions/transfer/TransferReview' -import { TransferTokenForm } from 'wallet/src/features/transactions/transfer/TransferTokenForm' -import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' -import { TransactionFlowProps, TransactionStep } from 'wallet/src/features/transactions/types' import { ANIMATE_SPRING_CONFIG } from 'wallet/src/features/transactions/utils' -import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' -import { ModalName, SectionName } from 'wallet/src/telemetry/constants' -import { currencyAddress } from 'wallet/src/utils/currencyId' +import { DerivedSwapInfo } from './swap/types' +import { TransactionFlowProps, TransactionStep } from './types' type InnerContentProps = Pick< TransactionFlowProps, @@ -117,7 +108,7 @@ export function TransactionFlow({ - {/* Padding bottom must have a similar size to the handlebar + {/* Padding bottom must have a similar size to the handlebar height as 100% height doesn't include the handlebar height */} {step !== TransactionStep.SUBMITTED && ( @@ -310,47 +301,6 @@ function TransferInnerContent({ onReviewNext, onReviewPrev, }: TransferInnerContentProps): JSX.Element | null { - // TODO: move this up in the tree to mobile specific flow - const { walletNeedsRestore, openWalletRestoreModal } = useWalletRestore() - const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = - useShouldShowNativeKeyboard() - - const { currencyAmounts, recipient, currencyInInfo, nftIn, chainId, txId } = derivedTransferInfo - const transferERC20Callback = useTransferERC20Callback( - txId, - chainId, - recipient, - currencyInInfo ? currencyAddress(currencyInInfo.currency) : undefined, - currencyAmounts[CurrencyField.INPUT]?.quotient.toString(), - txRequest, - onReviewNext - ) - const transferNFTCallback = useTransferNFTCallback( - txId, - chainId, - recipient, - nftIn?.nftContract?.address, - nftIn?.tokenId, - txRequest, - onReviewNext - ) - - const onTransfer = (): void => { - onFormNext() - nftIn ? transferNFTCallback?.() : transferERC20Callback?.() - } - - const { trigger: biometricAuthAndTransfer } = useBiometricPrompt(onTransfer) - const { requiredForTransactions: biometricRequired } = useBiometricAppSettings() - - const onReviewSubmit = async (): Promise => { - if (biometricRequired) { - await biometricAuthAndTransfer() - } else { - onTransfer() - } - } - switch (step) { case TransactionStep.SUBMITTED: return ( @@ -368,14 +318,8 @@ function TransferInnerContent({ @@ -388,8 +332,8 @@ function TransferInnerContent({ gasFee={gasFee} txRequest={txRequest} warnings={warnings} + onNext={onReviewNext} onPrev={onReviewPrev} - onReviewSubmit={onReviewSubmit} /> ) diff --git a/apps/mobile/src/features/transactions/TransactionFlowHeaderContent.tsx b/apps/mobile/src/features/transactions/TransactionFlowHeaderContent.tsx index 30e2bd7ca7e..a94312d2ee9 100644 --- a/apps/mobile/src/features/transactions/TransactionFlowHeaderContent.tsx +++ b/apps/mobile/src/features/transactions/TransactionFlowHeaderContent.tsx @@ -1,17 +1,17 @@ import React, { Dispatch, SetStateAction } from 'react' import { useTranslation } from 'react-i18next' import { Keyboard } from 'react-native' +import { ElementName } from 'src/features/telemetry/constants' +import { useTokenFormActionHandlers } from 'src/features/transactions/hooks' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import EyeIcon from 'ui/src/assets/icons/eye.svg' import SettingsIcon from 'ui/src/assets/icons/settings.svg' import { iconSizes } from 'ui/src/theme' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { useTokenFormActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenFormActionHandlers' -import { TransactionFlowProps, TransactionStep } from 'wallet/src/features/transactions/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' -import { ElementName } from 'wallet/src/telemetry/constants' +import { TransactionFlowProps, TransactionStep } from './types' type HeaderContentProps = Pick< TransactionFlowProps, diff --git a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.test.tsx b/apps/mobile/src/features/transactions/TransactionHistoryUpdater.test.tsx similarity index 92% rename from packages/wallet/src/features/transactions/TransactionHistoryUpdater.test.tsx rename to apps/mobile/src/features/transactions/TransactionHistoryUpdater.test.tsx index 9319e49af9e..aa95d3478db 100644 --- a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.test.tsx +++ b/apps/mobile/src/features/transactions/TransactionHistoryUpdater.test.tsx @@ -1,5 +1,11 @@ import dayjs from 'dayjs' import MockDate from 'mockdate' +import React from 'react' +import { + getReceiveNotificationFromData, + TransactionHistoryUpdater, +} from 'src/features/transactions/TransactionHistoryUpdater' +import { render } from 'src/test/test-utils' import { ChainId } from 'wallet/src/constants/chains' import { AssetActivity, Resolvers } from 'wallet/src/data/__generated__/types-and-hooks' import { AssetType } from 'wallet/src/entities/assets' @@ -7,27 +13,16 @@ import { AppNotificationType } from 'wallet/src/features/notifications/types' import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' import { Account } from 'wallet/src/features/wallet/accounts/types' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' +import { account, account2, faker, SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures' import { MAX_FIXTURE_TIMESTAMP, - SAMPLE_SEED_ADDRESS_1, - account, - account2, - faker, -} from 'wallet/src/test/fixtures' -import { Portfolios, PortfoliosWithReceive } from 'wallet/src/test/gqlFixtures' -import { render } from 'wallet/src/test/test-utils' -import { - TransactionHistoryUpdater, - getReceiveNotificationFromData, -} from './TransactionHistoryUpdater' + Portfolios, + PortfoliosWithReceive, +} from 'wallet/src/test/gqlFixtures' const mockedRefetchQueries = jest.fn() - -jest.mock('@apollo/client', () => ({ - ...jest.requireActual('@apollo/client'), - useApolloClient: jest.fn((): { refetchQueries: jest.Mock } => ({ - refetchQueries: mockedRefetchQueries, - })), +jest.mock('src/data/usePersistedApolloClient', () => ({ + apolloClient: { refetchQueries: mockedRefetchQueries }, })) const present = dayjs('2022-02-01') @@ -143,7 +138,6 @@ describe(TransactionHistoryUpdater, () => { const notificationStatusState = tree.store.getState().notifications.notificationStatus expect(notificationStatusState[account.address]).toBeTruthy() expect(notificationStatusState[account2.address]).toBeTruthy() - expect(mockedRefetchQueries).toHaveBeenCalled() }) it('does not update notification status when there are no new transactions', async () => { @@ -213,7 +207,7 @@ describe(getReceiveNotificationFromData, () => { chainId: ChainId.Mainnet, txHash: PortfoliosWithReceive[0].assetActivities[0]?.details.hash, // generated address: account.address, - txId: '0x9b0e1021d79e2a85b7a419f47cfa364ea6ae10bf', + txId: '0x80cde0e2abd1bf5fadcf7ff9edf7ae13feec1c32', type: AppNotificationType.Transaction, txType: TransactionType.Receive, assetType: AssetType.Currency, diff --git a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.tsx b/apps/mobile/src/features/transactions/TransactionHistoryUpdater.tsx similarity index 93% rename from packages/wallet/src/features/transactions/TransactionHistoryUpdater.tsx rename to apps/mobile/src/features/transactions/TransactionHistoryUpdater.tsx index 8591c490cb3..7472fc6ecdf 100644 --- a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.tsx +++ b/apps/mobile/src/features/transactions/TransactionHistoryUpdater.tsx @@ -1,28 +1,27 @@ -import { useApolloClient } from '@apollo/client' +// causing lint job to fail +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import dayjs from 'dayjs' -import { useEffect, useMemo } from 'react' +import React, { useEffect, useMemo } from 'react' import { View } from 'react-native' import { batch } from 'react-redux' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { apolloClient } from 'src/data/usePersistedApolloClient' +import { buildReceiveNotification } from 'src/features/notifications/buildReceiveNotification' +import { selectLastTxNotificationUpdate } from 'src/features/notifications/selectors' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { PollingInterval } from 'wallet/src/constants/misc' +import { GQLQueries } from 'wallet/src/data/queries' import { TransactionHistoryUpdaterQueryResult, TransactionListQuery, useTransactionHistoryUpdaterQuery, useTransactionListLazyQuery, } from 'wallet/src/data/__generated__/types-and-hooks' -import { GQLQueries } from 'wallet/src/data/queries' -import { buildReceiveNotification } from 'wallet/src/features/notifications/buildReceiveNotification' -import { selectLastTxNotificationUpdate } from 'wallet/src/features/notifications/selectors' import { pushNotification, setLastTxNotificationUpdate, setNotificationStatus, } from 'wallet/src/features/notifications/slice' -import { - ReceiveCurrencyTxNotification, - ReceiveNFTNotification, -} from 'wallet/src/features/notifications/types' import { parseDataResponseToTransactionDetails } from 'wallet/src/features/transactions/history/utils' import { useSelectAddressTransactions } from 'wallet/src/features/transactions/selectors' import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' @@ -32,7 +31,6 @@ import { useHideSpamTokensSetting, } from 'wallet/src/features/wallet/hooks' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' /** * For all imported accounts, checks for new transactions and updates @@ -105,7 +103,6 @@ function AddressTransactionHistoryUpdater({ > }): JSX.Element | null { const dispatch = useAppDispatch() - const apolloClient = useApolloClient() const activeAccountAddress = useAppSelector(selectActiveAccountAddress) @@ -175,7 +172,6 @@ function AddressTransactionHistoryUpdater({ activeAccountAddress, activities, address, - apolloClient, dispatch, fetchAndDispatchReceiveNotification, hideSpamTokens, @@ -231,7 +227,7 @@ export function getReceiveNotificationFromData( address: Address, lastTxNotificationUpdateTimestamp: number | undefined, hideSpamTokens = false -): ReceiveCurrencyTxNotification | ReceiveNFTNotification | undefined { +) { if (!data || !lastTxNotificationUpdateTimestamp) { return } diff --git a/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx b/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx index e80d94d96c5..b86fe1acdcb 100644 --- a/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx +++ b/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx @@ -1,15 +1,15 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { ElementName } from 'src/features/telemetry/constants' import { StatusAnimation } from 'src/features/transactions/TransactionPending/StatusAnimation' +import { openTransactionLink } from 'src/utils/linking' import { AnimatedFlex, Button, Flex, Text, TouchableArea } from 'ui/src' import { ChainId } from 'wallet/src/constants/chains' import { + isFinalizedTx, TransactionDetails, TransactionStatus, - isFinalizedTx, } from 'wallet/src/features/transactions/types' -import { ElementName } from 'wallet/src/telemetry/constants' -import { openTransactionLink } from 'wallet/src/utils/linking' type TransactionStatusProps = { transaction: TransactionDetails | undefined diff --git a/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx b/apps/mobile/src/features/transactions/TransactionReview.tsx similarity index 81% rename from packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx rename to apps/mobile/src/features/transactions/TransactionReview.tsx index 69b6114dcc4..9bf4638b471 100644 --- a/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx +++ b/apps/mobile/src/features/transactions/TransactionReview.tsx @@ -1,32 +1,36 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { ReactNode } from 'react' +import { notificationAsync } from 'expo-haptics' +import React, { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { FadeInUp, FadeOut } from 'react-native-reanimated' -import { isWeb } from 'tamagui' -import { AnimatedFlex, Button, Flex, Text, useDeviceDimensions, useMedia } from 'ui/src' -import { BackArrow } from 'ui/src/components/icons/BackArrow' +import { AddressDisplay } from 'src/components/AddressDisplay' +import { TransferArrowButton } from 'src/components/buttons/TransferArrowButton' +import { Arrow } from 'src/components/icons/Arrow' +import { AmountInput } from 'src/components/input/AmountInput' +import { RecipientPrevTransfers } from 'src/components/input/RecipientInputPanel' +import { TextInputProps } from 'src/components/input/TextInput' +import { NFTTransfer } from 'src/components/NFT/NFTTransfer' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { ElementName } from 'src/features/telemetry/constants' +import { + AnimatedFlex, + Button, + Flex, + Text, + useDeviceDimensions, + useMedia, + useSporeColors, +} from 'ui/src' import { fonts, iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { TransferArrowButton } from 'wallet/src/components/buttons/TransferArrowButton' import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo' -import { AmountInput } from 'wallet/src/components/input/AmountInput' -import { RecipientPrevTransfers } from 'wallet/src/components/input/RecipientInputPanel' -import { TextInputProps } from 'wallet/src/components/input/TextInput' -import { NFTTransfer } from 'wallet/src/components/NFT/NFTTransfer' import { CurrencyInfo } from 'wallet/src/features/dataApi/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' -import { ElementNameType } from 'wallet/src/telemetry/constants' import { getSymbolDisplayText } from 'wallet/src/utils/currency' interface BaseReviewProps { - actionButtonProps: { - disabled: boolean - label: string - name: ElementNameType - onPress: () => void - } + actionButtonProps: { disabled: boolean; label: string; name: ElementName; onPress: () => void } isFiatInput?: boolean transactionDetails?: ReactNode nftIn?: GQLNftAsset @@ -69,11 +73,15 @@ export function TransactionReview({ usdTokenEquivalentAmount, onPrev, }: TransactionReviewProps): JSX.Element { + const colors = useSporeColors() const media = useMedia() const { fullHeight } = useDeviceDimensions() const { t } = useTranslation() const { convertFiatAmountFormatted } = useLocalizationContext() + const { trigger: actionButtonTrigger } = useBiometricPrompt(actionButtonProps.onPress) + const { requiredForTransactions } = useBiometricAppSettings() + const textProps: TextInputProps = media.short ? { fontFamily: '$heading', @@ -110,8 +118,7 @@ export function TransactionReview({ grow $short={{ gap: '$none' }} entering={FadeInUp} - // TODO(EXT-526): re-enable `exiting` animation when it's fixed. - exiting={isWeb ? undefined : FadeOut} + exiting={FadeOut} gap="$spacing4"> {currencyInInfo ? ( @@ -205,19 +212,30 @@ export function TransactionReview({ {transactionDetails} - diff --git a/packages/wallet/src/features/transactions/hooks.ts b/apps/mobile/src/features/transactions/hooks.ts similarity index 59% rename from packages/wallet/src/features/transactions/hooks.ts rename to apps/mobile/src/features/transactions/hooks.ts index 79bacc91fc6..6469eca3e91 100644 --- a/packages/wallet/src/features/transactions/hooks.ts +++ b/apps/mobile/src/features/transactions/hooks.ts @@ -1,7 +1,19 @@ +import { AnyAction } from '@reduxjs/toolkit' import { Currency } from '@uniswap/sdk-core' import { BigNumberish } from 'ethers' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' +import { SearchContext } from 'src/components/explore/search/SearchContext' +import { flowToModalName } from 'src/components/TokenSelector/flowToModalName' +import { TokenSelectorFlow } from 'src/components/TokenSelector/types' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { + createSwapFormFromTxDetails, + createWrapFormFromTxDetails, +} from 'src/features/transactions/swap/createSwapFormFromTxDetails' +import { transactionStateActions } from 'src/features/transactions/transactionState/transactionState' import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { makeSelectTransaction, @@ -9,19 +21,18 @@ import { } from 'wallet/src/features/transactions/selectors' import { finalizeTransaction } from 'wallet/src/features/transactions/slice' import { - createSwapFormFromTxDetails, - createWrapFormFromTxDetails, -} from 'wallet/src/features/transactions/swap/createSwapFormFromTxDetails' -import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' import { + isFinalizedTx, TransactionDetails, TransactionStatus, TransactionType, - isFinalizedTx, } from 'wallet/src/features/transactions/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useAppDispatch, useAppSelector } from 'wallet/src/state' -import { areCurrencyIdsEqual, buildCurrencyId } from 'wallet/src/utils/currencyId' +import { areCurrencyIdsEqual, buildCurrencyId, currencyAddress } from 'wallet/src/utils/currencyId' export function usePendingTransactions( address: Address | null, @@ -119,6 +130,135 @@ export function useCreateWrapFormState( }, [chainId, inputCurrency, outputCurrency, transaction, txId]) } +export function useTokenSelectorActionHandlers( + dispatch: React.Dispatch, + flow: TokenSelectorFlow +): { + onShowTokenSelector: (field: CurrencyField) => void + onHideTokenSelector: () => void + onSelectCurrency: (currency: Currency, field: CurrencyField, context: SearchContext) => void +} { + const onShowTokenSelector = useCallback( + (field: CurrencyField) => dispatch(transactionStateActions.showTokenSelector(field)), + [dispatch] + ) + + const onHideTokenSelector = useCallback( + () => dispatch(transactionStateActions.showTokenSelector(undefined)), + [dispatch] + ) + + const onSelectCurrency = useCallback( + (currency: Currency, field: CurrencyField, context: SearchContext) => { + dispatch( + transactionStateActions.selectCurrency({ + field, + tradeableAsset: { + address: currencyAddress(currency), + chainId: currency.chainId, + type: AssetType.Currency, + }, + }) + ) + + // log event that a currency was selected + sendMobileAnalyticsEvent(MobileEventName.TokenSelected, { + name: currency.name, + address: currencyAddress(currency), + chain: currency.chainId, + modal: flowToModalName(flow), + field, + category: context.category, + position: context.position, + suggestion_count: context.suggestionCount, + query: context.query, + }) + + // hide screen when done selecting + onHideTokenSelector() + }, + [dispatch, flow, onHideTokenSelector] + ) + return { onSelectCurrency, onShowTokenSelector, onHideTokenSelector } +} + +/** Set of handlers wrapping actions involving user input */ +export function useTokenFormActionHandlers(dispatch: React.Dispatch): { + onCreateTxId: (txId: string) => void + onFocusInput: () => void + onFocusOutput: () => void + onSwitchCurrencies: () => void + onToggleFiatInput: (isFiatInput: boolean) => void + onSetExactAmount: (field: CurrencyField, value: string, isFiatInput?: boolean) => void + onSetMax: (amount: string) => void +} { + const onUpdateExactTokenAmount = useCallback( + (field: CurrencyField, amount: string) => + dispatch(transactionStateActions.updateExactAmountToken({ field, amount })), + [dispatch] + ) + + const onUpdateExactUSDAmount = useCallback( + (field: CurrencyField, amount: string) => + dispatch(transactionStateActions.updateExactAmountFiat({ field, amount })), + [dispatch] + ) + + const onSetExactAmount = useCallback( + (field: CurrencyField, value: string, isFiatInput?: boolean) => { + const updater = isFiatInput ? onUpdateExactUSDAmount : onUpdateExactTokenAmount + updater(field, value) + }, + [onUpdateExactUSDAmount, onUpdateExactTokenAmount] + ) + + const onSetMax = useCallback( + (amount: string) => { + // when setting max amount, always switch to token mode because + // our token/usd updater doesnt handle this case yet + dispatch(transactionStateActions.toggleFiatInput(false)) + dispatch( + transactionStateActions.updateExactAmountToken({ field: CurrencyField.INPUT, amount }) + ) + // Unfocus the CurrencyInputField by setting focusOnCurrencyField to null + dispatch(transactionStateActions.onFocus(null)) + }, + [dispatch] + ) + + const onSwitchCurrencies = useCallback(() => { + dispatch(transactionStateActions.switchCurrencySides()) + }, [dispatch]) + + const onToggleFiatInput = useCallback( + (isFiatInput: boolean) => dispatch(transactionStateActions.toggleFiatInput(isFiatInput)), + [dispatch] + ) + + const onCreateTxId = useCallback( + (txId: string) => dispatch(transactionStateActions.setTxId(txId)), + [dispatch] + ) + + const onFocusInput = useCallback( + () => dispatch(transactionStateActions.onFocus(CurrencyField.INPUT)), + [dispatch] + ) + const onFocusOutput = useCallback( + () => dispatch(transactionStateActions.onFocus(CurrencyField.OUTPUT)), + [dispatch] + ) + return { + onCreateTxId, + onFocusInput, + onFocusOutput, + onSwitchCurrencies, + onToggleFiatInput, + onSetExactAmount, + onSetMax, + } +} + /** * Merge local and remote transactions. If duplicated hash found use data from local store. */ @@ -257,3 +397,25 @@ export function useLowestPendingNonce(): BigNumberish | undefined { return min }, [pending]) } + +/** + * Gets all transactions from a given sender and to a given recipient + * @param sender Get all transactions sent by this sender + * @param recipient Then filter so that we only keep txns to this recipient + */ +export function useAllTransactionsBetweenAddresses( + sender: Address, + recipient: Maybe
+): TransactionDetails[] | undefined { + const txnsToSearch = useSelectAddressTransactions(sender) + return useMemo(() => { + if (!sender || !recipient || !txnsToSearch) { + return + } + + return txnsToSearch.filter( + (tx: TransactionDetails) => + tx.typeInfo.type === TransactionType.Send && tx.typeInfo.recipient === recipient + ) + }, [recipient, sender, txnsToSearch]) +} diff --git a/apps/mobile/src/features/transactions/hooks/useOnSendEmptyActionPress.ts b/apps/mobile/src/features/transactions/hooks/useOnSendEmptyActionPress.ts deleted file mode 100644 index e72e95faf21..00000000000 --- a/apps/mobile/src/features/transactions/hooks/useOnSendEmptyActionPress.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useCallback } from 'react' -import { ScannerModalState } from 'src/components/QRCodeScanner/constants' -import { closeModal, openModal } from 'src/features/modals/modalSlice' -import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' -import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' - -export function useOnSendEmptyActionPress(): () => void { - const { data } = useFiatOnRampIpAddressQuery() - const dispatch = useAppDispatch() - const fiatOnRampEligible = Boolean(data?.isBuyAllowed) - - return useCallback((): void => { - dispatch(closeModal({ name: ModalName.Send })) - if (fiatOnRampEligible) { - dispatch(openModal({ name: ModalName.FiatOnRamp })) - } else { - dispatch( - openModal({ - name: ModalName.WalletConnectScan, - initialState: ScannerModalState.WalletQr, - }) - ) - } - }, [dispatch, fiatOnRampEligible]) -} diff --git a/packages/wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo.tsx b/apps/mobile/src/features/transactions/swap/FeeOnTransferInfo.tsx similarity index 98% rename from packages/wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo.tsx rename to apps/mobile/src/features/transactions/swap/FeeOnTransferInfo.tsx index 1331018f6c5..931db6e3d5a 100644 --- a/packages/wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo.tsx +++ b/apps/mobile/src/features/transactions/swap/FeeOnTransferInfo.tsx @@ -1,4 +1,5 @@ import { Percent } from '@uniswap/sdk-core' +import React from 'react' import { useTranslation } from 'react-i18next' import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' diff --git a/apps/mobile/src/features/transactions/swap/SwapArrowButton.tsx b/apps/mobile/src/features/transactions/swap/SwapArrowButton.tsx index 328d7d84947..834c357d152 100644 --- a/apps/mobile/src/features/transactions/swap/SwapArrowButton.tsx +++ b/apps/mobile/src/features/transactions/swap/SwapArrowButton.tsx @@ -1,7 +1,7 @@ import React from 'react' +import { Arrow } from 'src/components/icons/Arrow' import { Flex, TouchableArea, TouchableAreaProps, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { Arrow } from 'wallet/src/components/icons/Arrow' type SwapArrowButtonProps = Pick< TouchableAreaProps, diff --git a/packages/wallet/src/features/transactions/swap/SwapDetails.tsx b/apps/mobile/src/features/transactions/swap/SwapDetails.tsx similarity index 93% rename from packages/wallet/src/features/transactions/swap/SwapDetails.tsx rename to apps/mobile/src/features/transactions/swap/SwapDetails.tsx index db8ac3b767c..88753c77b0c 100644 --- a/packages/wallet/src/features/transactions/swap/SwapDetails.tsx +++ b/apps/mobile/src/features/transactions/swap/SwapDetails.tsx @@ -2,23 +2,24 @@ import { Currency, TradeType } from '@uniswap/sdk-core' import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { TouchableOpacity } from 'react-native-gesture-handler' -import { Flex, Icons, Text, TouchableArea } from 'ui/src' +import { Warning } from 'src/components/modals/WarningModal/types' +import { OnShowSwapFeeInfo } from 'src/components/SwapFee/SwapFee' +import Trace from 'src/components/Trace/Trace' +import { CurrencyInfo } from 'src/features/dataApi/types' +import { ElementName } from 'src/features/telemetry/constants' +import { FeeOnTransferInfo } from 'src/features/transactions/swap/FeeOnTransferInfo' +import { DerivedSwapInfo } from 'src/features/transactions/swap/types' +import { getRateToDisplay } from 'src/features/transactions/swap/utils' +import { TransactionDetails } from 'src/features/transactions/TransactionDetails' +import { Flex, Text, TouchableArea } from 'ui/src' +import { InfoCircleFilled } from 'ui/src/components/icons' import { NumberType } from 'utilities/src/format/types' -import { Trace } from 'utilities/src/telemetry/trace/Trace' -import { CurrencyInfo } from 'wallet/src/features/dataApi/types' import { useSwapRewriteEnabled } from 'wallet/src/features/experiments/hooks' import { GasFeeResult } from 'wallet/src/features/gas/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useUSDCPrice } from 'wallet/src/features/routing/useUSDCPrice' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { Trade } from 'wallet/src/features/transactions/swap/useTrade' -import { getRateToDisplay } from 'wallet/src/features/transactions/swap/utils' -import { FeeOnTransferInfo } from 'wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo' -import { OnShowSwapFeeInfo } from 'wallet/src/features/transactions/TransactionDetails/SwapFee' -import { TransactionDetails } from 'wallet/src/features/transactions/TransactionDetails/TransactionDetails' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { Warning } from 'wallet/src/features/transactions/WarningModal/types' -import { ElementName } from 'wallet/src/telemetry/constants' import { getFormattedCurrencyAmount, getSymbolDisplayText } from 'wallet/src/utils/currency' import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' @@ -206,7 +207,7 @@ export function SwapDetails({ {t('Max slippage')}   - + diff --git a/apps/mobile/src/features/transactions/swap/SwapFlow.tsx b/apps/mobile/src/features/transactions/swap/SwapFlow.tsx index 58b8329c8d4..546c7d082ae 100644 --- a/apps/mobile/src/features/transactions/swap/SwapFlow.tsx +++ b/apps/mobile/src/features/transactions/swap/SwapFlow.tsx @@ -1,28 +1,25 @@ import React, { useEffect, useMemo, useReducer, useState } from 'react' import { useTranslation } from 'react-i18next' -import { TransactionFlow } from 'src/features/transactions/TransactionFlow' +import { WarningAction } from 'src/components/modals/WarningModal/types' import { TokenSelectorModal, TokenSelectorVariation, -} from 'wallet/src/components/TokenSelector/TokenSelector' -import { useSwapWarnings } from 'wallet/src/features/transactions/hooks/useSwapWarnings' -import { useTokenSelectorActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers' -import { useTransactionGasWarning } from 'wallet/src/features/transactions/hooks/useTransactionGasWarning' -import { - useDerivedSwapInfo, - useSwapTxAndGasInfoLegacy, -} from 'wallet/src/features/transactions/swap/hooks' +} from 'src/components/TokenSelector/TokenSelector' +import { TokenSelectorFlow } from 'src/components/TokenSelector/types' +import { useTokenSelectorActionHandlers } from 'src/features/transactions/hooks' +import { useDerivedSwapInfo, useSwapTxAndGasInfo } from 'src/features/transactions/swap/hooks' +import { useSwapWarnings } from 'src/features/transactions/swap/useSwapWarnings' +import { TransactionFlow } from 'src/features/transactions/TransactionFlow' import { initialState as emptyState, transactionStateReducer, -} from 'wallet/src/features/transactions/transactionState/transactionState' +} from 'src/features/transactions/transactionState/transactionState' +import { TransactionStep } from 'src/features/transactions/types' +import { useTransactionGasWarning } from 'src/features/transactions/useTransactionGasWarning' import { CurrencyField, TransactionState, } from 'wallet/src/features/transactions/transactionState/types' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' -import { TransactionStep } from 'wallet/src/features/transactions/types' -import { WarningAction } from 'wallet/src/features/transactions/WarningModal/types' interface SwapFormProps { prefilledState?: TransactionState @@ -45,15 +42,12 @@ export function SwapFlow({ prefilledState, onClose }: SwapFormProps): JSX.Elemen const [step, setStep] = useState(TransactionStep.FORM) const warnings = useSwapWarnings(t, derivedSwapInfo) - - // Force this legacy swap flow to use the old routing api logic, as we're planning to remove this, and splitting the code is complex. - const { txRequest, approveTxRequest, gasFee } = useSwapTxAndGasInfoLegacy({ + const { txRequest, approveTxRequest, gasFee } = useSwapTxAndGasInfo({ derivedSwapInfo, skipGasFeeQuery: step === TransactionStep.SUBMITTED || warnings.some((warning) => warning.action === WarningAction.DisableReview), }) - const gasWarning = useTransactionGasWarning({ derivedInfo: derivedSwapInfo, gasFee: gasFee.value, @@ -64,10 +58,9 @@ export function SwapFlow({ prefilledState, onClose }: SwapFormProps): JSX.Elemen }, [warnings, gasWarning]) // keep currencies list option as state so that rendered list remains stable through the slide animation - const [listVariation, setListVariation] = useState< - | TokenSelectorVariation.BalancesAndPopular - | TokenSelectorVariation.SuggestedAndFavoritesAndPopular - >(TokenSelectorVariation.BalancesAndPopular) + const [listVariation, setListVariation] = useState( + TokenSelectorVariation.BalancesAndPopular + ) useEffect(() => { if (selectingCurrencyField) { diff --git a/apps/mobile/src/features/transactions/swap/SwapForm.tsx b/apps/mobile/src/features/transactions/swap/SwapForm.tsx index bea887d062a..b6bb6ddc23f 100644 --- a/apps/mobile/src/features/transactions/swap/SwapForm.tsx +++ b/apps/mobile/src/features/transactions/swap/SwapForm.tsx @@ -6,41 +6,38 @@ import { useTranslation } from 'react-i18next' import { Keyboard, StyleSheet, TextInputProps } from 'react-native' import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated' import { useShouldShowNativeKeyboard } from 'src/app/hooks' +import { CurrencyInputPanel } from 'src/components/input/CurrencyInputPanel' +import { DecimalPad } from 'src/components/input/DecimalPad' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { Warning, WarningAction, WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal, { getAlertColor } from 'src/components/modals/WarningModal/WarningModal' +import { TokenSelectorFlow } from 'src/components/TokenSelector/types' import Trace from 'src/components/Trace/Trace' +import { ElementName, ModalName, SectionName } from 'src/features/telemetry/constants' +import { + useTokenFormActionHandlers, + useTokenSelectorActionHandlers, +} from 'src/features/transactions/hooks' +import { useSwapAnalytics } from 'src/features/transactions/swap/analytics' +import { useShowSwapNetworkNotification } from 'src/features/transactions/swap/hooks' import { SwapArrowButton } from 'src/features/transactions/swap/SwapArrowButton' +import { isPriceImpactWarning } from 'src/features/transactions/swap/useSwapWarnings' +import { getRateToDisplay, isWrapAction } from 'src/features/transactions/swap/utils' +import { BlockedAddressWarning } from 'src/features/trm/BlockedAddressWarning' import { useWalletRestore } from 'src/features/wallet/hooks' import { AnimatedFlex, Button, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import InfoCircleFilled from 'ui/src/assets/icons/info-circle-filled.svg' import InfoCircle from 'ui/src/assets/icons/info-circle.svg' import { iconSizes, spacing } from 'ui/src/theme' -import { CurrencyInputPanelLegacy } from 'wallet/src/components/legacy/CurrencyInputPanelLegacy' -import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy' -import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' -import { getAlertColor, WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { useSwapAnalytics } from 'wallet/src/features/transactions/swap/analytics' -import { BlockedAddressWarning } from 'wallet/src/features/trm/BlockedAddressWarning' -import { ModalName, SectionName } from 'wallet/src/telemetry/constants' // eslint-disable-next-line no-restricted-imports import { formatCurrencyAmount } from 'utilities/src/format/localeBased' import { NumberType } from 'utilities/src/format/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useUSDCPrice } from 'wallet/src/features/routing/useUSDCPrice' -import { isPriceImpactWarning } from 'wallet/src/features/transactions/hooks/useSwapWarnings' -import { useTokenFormActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenFormActionHandlers' -import { useTokenSelectorActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers' -import { useShowSwapNetworkNotification } from 'wallet/src/features/transactions/swap/hooks' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { getRateToDisplay, isWrapAction } from 'wallet/src/features/transactions/swap/utils' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' import { createTransactionId } from 'wallet/src/features/transactions/utils' -import { - Warning, - WarningAction, - WarningSeverity, -} from 'wallet/src/features/transactions/WarningModal/types' import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' -import { ElementName } from 'wallet/src/telemetry/constants' +import { DerivedSwapInfo } from './types' interface SwapFormProps { dispatch: Dispatch @@ -248,7 +245,7 @@ function _SwapForm({ onLayout={onInputPanelLayout}> - - {!showNativeKeyboard && ( - void @@ -125,39 +119,24 @@ export function SwapReview({ txId ) - const onSwapOrWrap = useCallback(() => { - return isWrapAction(wrapType) ? onWrap() : onSwap() - }, [onSwap, onWrap, wrapType]) - - const { trigger: biometricAuthAndTransfer } = useBiometricPrompt(onSwapOrWrap) - const { requiredForTransactions: biometricRequired } = useBiometricAppSettings() - - const onAuthAndSubmitTxn = useCallback(async () => { - if (biometricRequired) { - await biometricAuthAndTransfer() - } else { - onSwapOrWrap() - } - }, [biometricAuthAndTransfer, biometricRequired, onSwapOrWrap]) - - const onPress = useCallback(async () => { + const onPress = useCallback(() => { if (swapWarning && !showWarningModal && !warningAcknowledged) { setShouldSubmitTx(true) setShowWarningModal(true) return } - await notificationAsync() - await onAuthAndSubmitTxn() - }, [swapWarning, showWarningModal, warningAcknowledged, onAuthAndSubmitTxn]) - const onConfirmWarning = useCallback(async () => { + isWrapAction(wrapType) ? onWrap() : onSwap() + }, [warningAcknowledged, swapWarning, showWarningModal, wrapType, onWrap, onSwap]) + + const onConfirmWarning = useCallback(() => { setWarningAcknowledged(true) setShowWarningModal(false) if (shouldSubmitTx) { - await onAuthAndSubmitTxn() + isWrapAction(wrapType) ? onWrap() : onSwap() } - }, [shouldSubmitTx, onAuthAndSubmitTxn]) + }, [wrapType, onWrap, onSwap, shouldSubmitTx]) const onCancelWarning = useCallback(() => { setShowWarningModal(false) diff --git a/apps/mobile/src/features/transactions/swap/SwapStatus.tsx b/apps/mobile/src/features/transactions/swap/SwapStatus.tsx index f7c9d90b153..2da1267b277 100644 --- a/apps/mobile/src/features/transactions/swap/SwapStatus.tsx +++ b/apps/mobile/src/features/transactions/swap/SwapStatus.tsx @@ -1,6 +1,7 @@ import { TradeType } from '@uniswap/sdk-core' import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { useSelectTransaction } from 'src/features/transactions/hooks' import { TransactionPending } from 'src/features/transactions/TransactionPending/TransactionPending' import { AppTFunction } from 'ui/src/i18n/types' import { ChainId } from 'wallet/src/constants/chains' @@ -10,18 +11,17 @@ import { useLocalizationContext, } from 'wallet/src/features/language/LocalizationContext' import { getAmountsFromTrade } from 'wallet/src/features/transactions/getAmountsFromTrade' -import { useSelectTransaction } from 'wallet/src/features/transactions/hooks' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { + isConfirmedSwapTypeInfo, TransactionDetails, TransactionStatus, TransactionType, WrapType, - isConfirmedSwapTypeInfo, } from 'wallet/src/features/transactions/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { getFormattedCurrencyAmount, getSymbolDisplayText } from 'wallet/src/utils/currency' +import { DerivedSwapInfo } from './types' type SwapStatusProps = { derivedSwapInfo: DerivedSwapInfo diff --git a/packages/wallet/src/features/transactions/swap/analytics.ts b/apps/mobile/src/features/transactions/swap/analytics.ts similarity index 76% rename from packages/wallet/src/features/transactions/swap/analytics.ts rename to apps/mobile/src/features/transactions/swap/analytics.ts index 3e2f9cde519..085edf9f04c 100644 --- a/packages/wallet/src/features/transactions/swap/analytics.ts +++ b/apps/mobile/src/features/transactions/swap/analytics.ts @@ -1,19 +1,18 @@ import { SwapEventName } from '@uniswap/analytics-events' import { Currency, TradeType } from '@uniswap/sdk-core' import { useEffect, useRef } from 'react' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { NumberType } from 'utilities/src/format/types' import { LocalizationContextState, useLocalizationContext, } from 'wallet/src/features/language/LocalizationContext' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { QuoteData, Trade } from 'wallet/src/features/transactions/swap/useTrade' +import { Trade } from 'wallet/src/features/transactions/swap/useTrade' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { QuoteType } from 'wallet/src/features/transactions/utils' -import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' import { SwapTradeBaseProperties } from 'wallet/src/telemetry/types' import { currencyAddress, getCurrencyAddressForAnalytics } from 'wallet/src/utils/currencyId' -import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' +import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' +import { DerivedSwapInfo } from './types' // hook-based analytics because this one is data-lifecycle dependent export function useSwapAnalytics(derivedSwapInfo: DerivedSwapInfo): void { @@ -46,23 +45,20 @@ export function useSwapAnalytics(derivedSwapInfo: DerivedSwapInfo): void { return } - sendWalletAnalyticsEvent( + sendMobileAnalyticsEvent( SwapEventName.SWAP_QUOTE_RECEIVED, - getBaseTradeAnalyticsProperties({ formatter, trade: currTrade }) + getBaseTradeAnalyticsProperties(formatter, currTrade) ) }, [inputAmount, inputCurrencyId, outputCurrencyId, tradeType, formatter]) return } -export function getBaseTradeAnalyticsProperties({ - formatter, - trade, -}: { - formatter: LocalizationContextState +export function getBaseTradeAnalyticsProperties( + formatter: LocalizationContextState, trade: Trade -}): SwapTradeBaseProperties { - const portionAmount = getPortionAmountFromQuoteData(trade.quoteData) +): SwapTradeBaseProperties { + const portionAmount = trade.quote?.portionAmount const feeCurrencyAmount = getCurrencyAmount({ value: portionAmount, @@ -89,25 +85,19 @@ export function getBaseTradeAnalyticsProperties({ }), allowed_slippage_basis_points: trade.slippageTolerance * 100, fee_amount: portionAmount, - quoteType: trade.quoteData?.quoteType, - requestId: trade.quoteData?.quote?.requestId, } } -export function getBaseTradeAnalyticsPropertiesFromSwapInfo({ - derivedSwapInfo, - formatter, -}: { - derivedSwapInfo: DerivedSwapInfo +export function getBaseTradeAnalyticsPropertiesFromSwapInfo( + derivedSwapInfo: DerivedSwapInfo, formatter: LocalizationContextState -}): SwapTradeBaseProperties { +): SwapTradeBaseProperties { const { chainId, currencyAmounts } = derivedSwapInfo const inputCurrencyAmount = currencyAmounts[CurrencyField.INPUT] const outputCurrencyAmount = currencyAmounts[CurrencyField.OUTPUT] const slippageTolerance = derivedSwapInfo.customSlippageTolerance ?? derivedSwapInfo.autoSlippageTolerance - - const portionAmount = getPortionAmountFromQuoteData(derivedSwapInfo.trade.trade?.quoteData) + const portionAmount = derivedSwapInfo.trade.trade?.quote?.portionAmount const feeCurrencyAmount = getCurrencyAmount({ value: portionAmount, @@ -143,16 +133,3 @@ export function getBaseTradeAnalyticsPropertiesFromSwapInfo({ fee_amount: portionAmount, } } - -// Index into the quote response for portion amount based on the response type -function getPortionAmountFromQuoteData(quoteData?: QuoteData): string | undefined { - if (!quoteData?.quote) { - return undefined - } - - if (quoteData.quoteType === QuoteType.RoutingApi) { - return quoteData.quote.portionAmount - } - - return quoteData.quote.quote.portionAmount -} diff --git a/packages/wallet/src/features/transactions/swap/createSwapFormFromTxDetails.ts b/apps/mobile/src/features/transactions/swap/createSwapFormFromTxDetails.ts similarity index 98% rename from packages/wallet/src/features/transactions/swap/createSwapFormFromTxDetails.ts rename to apps/mobile/src/features/transactions/swap/createSwapFormFromTxDetails.ts index 7fdb98c1c5d..85ad34baba7 100644 --- a/packages/wallet/src/features/transactions/swap/createSwapFormFromTxDetails.ts +++ b/apps/mobile/src/features/transactions/swap/createSwapFormFromTxDetails.ts @@ -8,7 +8,7 @@ import { } from 'wallet/src/features/transactions/transactionState/types' import { TransactionDetails, TransactionType } from 'wallet/src/features/transactions/types' import { currencyAddress, currencyIdToAddress } from 'wallet/src/utils/currencyId' -import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' +import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' interface Props { transactionDetails: TransactionDetails diff --git a/packages/wallet/src/features/transactions/swap/hooks.ts b/apps/mobile/src/features/transactions/swap/hooks.ts similarity index 86% rename from packages/wallet/src/features/transactions/swap/hooks.ts rename to apps/mobile/src/features/transactions/swap/hooks.ts index 7c6702339c6..45dbc96e8c1 100644 --- a/packages/wallet/src/features/transactions/swap/hooks.ts +++ b/apps/mobile/src/features/transactions/swap/hooks.ts @@ -9,6 +9,29 @@ import { FeeOptions } from '@uniswap/v3-sdk' import { providers } from 'ethers' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AnyAction } from 'redux' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { setHasSubmittedHoldToSwap } from 'src/features/behaviorHistory/slice' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { selectSwapStartTimestamp } from 'src/features/telemetry/timing/selectors' +import { updateSwapStartTimestamp } from 'src/features/telemetry/timing/slice' +import { + getBaseTradeAnalyticsProperties, + getBaseTradeAnalyticsPropertiesFromSwapInfo, +} from 'src/features/transactions/swap/analytics' +import { swapActions } from 'src/features/transactions/swap/swapSaga' +import { + getSwapMethodParameters, + getWrapType, + isWrapAction, + requireAcceptNewTrade, + sumGasFees, +} from 'src/features/transactions/swap/utils' +import { getWethContract, tokenWrapActions } from 'src/features/transactions/swap/wrapSaga' +import { + updateExactAmountFiat, + updateExactAmountToken, +} from 'src/features/transactions/transactionState/transactionState' +import { toStringish } from 'src/utils/number' import { NumberType } from 'utilities/src/format/types' import { logger } from 'utilities/src/logger/logger' import { flattenObjectOfObjects } from 'utilities/src/primitives/objects' @@ -16,7 +39,6 @@ import { useAsyncData, usePrevious } from 'utilities/src/react/hooks' import ERC20_ABI from 'wallet/src/abis/erc20.json' import { Erc20 } from 'wallet/src/abis/types' import { ChainId } from 'wallet/src/constants/chains' -import { setHasSubmittedHoldToSwap } from 'wallet/src/features/behaviorHistory/slice' import { ContractManager } from 'wallet/src/features/contracts/ContractManager' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' @@ -26,7 +48,6 @@ import { useLocalizationContext } from 'wallet/src/features/language/Localizatio import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useOnChainCurrencyBalance } from 'wallet/src/features/portfolio/api' -import { NO_QUOTE_DATA } from 'wallet/src/features/routing/api' import { useSimulatedGasLimit } from 'wallet/src/features/routing/hooks' import { STABLECOIN_AMOUNT_OUT, @@ -35,14 +56,6 @@ import { } from 'wallet/src/features/routing/useUSDCPrice' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { selectTransactions } from 'wallet/src/features/transactions/selectors' -import { - getBaseTradeAnalyticsProperties, - getBaseTradeAnalyticsPropertiesFromSwapInfo, -} from 'wallet/src/features/transactions/swap/analytics' -import { SwapParams, swapActions } from 'wallet/src/features/transactions/swap/swapSaga' -import { useTradingApiTrade } from 'wallet/src/features/transactions/swap/tradingApi/useTradingApiTrade' -import { isClassicQuote } from 'wallet/src/features/transactions/swap/tradingApi/utils' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { PermitSignatureInfo, usePermit2Signature, @@ -52,23 +65,6 @@ import { useSetTradeSlippage, useTrade, } from 'wallet/src/features/transactions/swap/useTrade' -import { - getSwapMethodParameters, - getWrapType, - isWrapAction, - requireAcceptNewTrade, - sumGasFees, - tradeToTransactionInfo, -} from 'wallet/src/features/transactions/swap/utils' -import { - WrapParams, - getWethContract, - tokenWrapActions, -} from 'wallet/src/features/transactions/swap/wrapSaga' -import { - updateExactAmountFiat, - updateExactAmountToken, -} from 'wallet/src/features/transactions/transactionState/transactionState' import { CurrencyField, TransactionState, @@ -78,19 +74,14 @@ import { TransactionType, WrapType, } from 'wallet/src/features/transactions/types' -import { QuoteType } from 'wallet/src/features/transactions/utils' import { useContractManager, useProvider } from 'wallet/src/features/wallet/context' import { useActiveAccount, useActiveAccountAddressWithThrow, } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' -import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' -import { selectSwapStartTimestamp } from 'wallet/src/telemetry/timing/selectors' -import { updateSwapStartTimestamp } from 'wallet/src/telemetry/timing/slice' import { buildCurrencyId } from 'wallet/src/utils/currencyId' -import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' -import { toStringish } from 'wallet/src/utils/number' +import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' +import { DerivedSwapInfo } from './types' const NUM_DECIMALS_USD = 2 const NUM_DECIMALS_DISPLAY = 2 @@ -155,31 +146,23 @@ export function useDerivedSwapInfo(state: TransactionState): DerivedSwapInfo { }, [exactAmountToken, exactCurrency]) const shouldGetQuote = !isWrapAction(wrapType) + const sendPortionEnabled = useFeatureFlag(FEATURE_FLAGS.PortionFields) - const isTradingApiEnabled = useFeatureFlag(FEATURE_FLAGS.TradingApi) - const tradeParams = { + // Fetch the trade quote. If customSlippageTolerance is undefined, then the quote is fetched with MAX_AUTO_SLIPPAGE_TOLERANCE + const tradeWithoutSlippage = useTrade({ amountSpecified: shouldGetQuote ? amountSpecified : null, otherCurrency, tradeType: isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, customSlippageTolerance, sendPortionEnabled, - } - - const legacyTrade = useTrade({ - ...tradeParams, - skip: isTradingApiEnabled, - }) - - const tradingApiTrade = useTradingApiTrade({ - ...tradeParams, - skip: !isTradingApiEnabled, }) - const activeTrade = isTradingApiEnabled ? tradingApiTrade : legacyTrade - // Calculate auto slippage tolerance for trade. If customSlippageTolerance is undefined, then the Trade slippage is set to the calculated value. - const { trade, autoSlippageTolerance } = useSetTradeSlippage(activeTrade, customSlippageTolerance) + const { trade, autoSlippageTolerance } = useSetTradeSlippage( + tradeWithoutSlippage, + customSlippageTolerance + ) const currencyAmounts = useMemo( () => @@ -199,9 +182,9 @@ export function useDerivedSwapInfo(state: TransactionState): DerivedSwapInfo { [CurrencyField.OUTPUT]: amountSpecified, }, [ - shouldGetQuote, - exactCurrencyField, amountSpecified, + exactCurrencyField, + shouldGetQuote, trade.trade?.inputAmount, trade.trade?.outputAmount, ] @@ -231,11 +214,11 @@ export function useDerivedSwapInfo(state: TransactionState): DerivedSwapInfo { currencyAmounts, currencyAmountsUSDValue, currencyBalances, - trade, exactAmountToken, exactAmountFiat, exactCurrencyField, focusOnCurrencyField, + trade, wrapType, selectingCurrencyField, txId, @@ -243,21 +226,21 @@ export function useDerivedSwapInfo(state: TransactionState): DerivedSwapInfo { customSlippageTolerance, } }, [ - autoSlippageTolerance, chainId, currencies, currencyAmounts, - currencyAmountsUSDValue, currencyBalances, - customSlippageTolerance, - exactAmountFiat, + currencyAmountsUSDValue, exactAmountToken, + exactAmountFiat, exactCurrencyField, focusOnCurrencyField, selectingCurrencyField, trade, txId, wrapType, + autoSlippageTolerance, + customSlippageTolerance, ]) } @@ -338,14 +321,11 @@ export enum ApprovalAction { Permit = 'permit', Permit2Approve = 'permit2-approve', - - // Unable to fetch approval status, should block submission UI - Unknown = 'unknown', } -export type TokenApprovalInfo = +type TokenApprovalInfo = | { - action: ApprovalAction.None | ApprovalAction.Permit | ApprovalAction.Unknown + action: ApprovalAction.None | ApprovalAction.Permit txRequest: null } | { @@ -381,7 +361,7 @@ function useTransactionRequestInfo( } } -export function useWrapTransactionRequest( +function useWrapTransactionRequest( derivedSwapInfo: DerivedSwapInfo ): providers.TransactionRequest | undefined { const address = useActiveAccountAddressWithThrow() @@ -610,7 +590,7 @@ interface SwapTxAndGasInfo { gasFee: GasFeeResult } -export function useSwapTxAndGasInfoLegacy({ +export function useSwapTxAndGasInfo({ derivedSwapInfo, skipGasFeeQuery, }: { @@ -673,26 +653,19 @@ export function useSwapTxAndGasInfoLegacy({ if (swapGasFee.error && simulatedGasEstimateError) { if (shouldFetchSimulatedGasLimit) { const simulationError = - typeof simulatedGasEstimateError === 'boolean' + typeof simulatedGasEstimationInfo.error === 'boolean' ? new Error('Unknown gas simulation error') : simulatedGasEstimateError - const isNoQuoteDataError = - 'message' in simulationError && simulationError.message === NO_QUOTE_DATA - - // We do not want to log to Sentry if it's a liquidity error. - if (!isNoQuoteDataError) { - logger.error(simulationError, { - tags: { file: 'swap/hooks', function: 'useSwapTxAndGasInfo' }, - extra: { - requestId: simulatedGasEstimateRequestId, - quoteId: simulatedGasEstimateQuoteId, - }, - }) - } - - sendWalletAnalyticsEvent(SwapEventName.SWAP_ESTIMATE_GAS_CALL_FAILED, { - ...getBaseTradeAnalyticsPropertiesFromSwapInfo({ derivedSwapInfo, formatter }), + logger.error(simulationError, { + tags: { file: 'swap/hooks', function: 'useSwapTxAndGasInfo' }, + extra: { + requestId: simulatedGasEstimateRequestId, + quoteId: simulatedGasEstimateQuoteId, + }, + }) + sendMobileAnalyticsEvent(SwapEventName.SWAP_ESTIMATE_GAS_CALL_FAILED, { + ...getBaseTradeAnalyticsPropertiesFromSwapInfo(derivedSwapInfo, formatter), error: simulationError.toString(), txRequest: transactionRequest, }) @@ -700,9 +673,8 @@ export function useSwapTxAndGasInfoLegacy({ logger.error(swapGasFee.error, { tags: { file: 'swap/hooks', function: 'useSwapTxAndGasInfo' }, }) - - sendWalletAnalyticsEvent(SwapEventName.SWAP_ESTIMATE_GAS_CALL_FAILED, { - ...getBaseTradeAnalyticsPropertiesFromSwapInfo({ derivedSwapInfo, formatter }), + sendMobileAnalyticsEvent(SwapEventName.SWAP_ESTIMATE_GAS_CALL_FAILED, { + ...getBaseTradeAnalyticsPropertiesFromSwapInfo(derivedSwapInfo, formatter), error: swapGasFee.error.toString(), txRequest: transactionRequest, }) @@ -780,18 +752,18 @@ export function useSwapCallback( onSubmit: () => void, txId?: string, isHoldToSwap?: boolean, - isFiatInputMode?: boolean ): () => void { const appDispatch = useAppDispatch() const account = useActiveAccount() const formatter = useLocalizationContext() + const swapStartTimestamp = useAppSelector(selectSwapStartTimestamp) return useMemo(() => { if (!account || !swapTxRequest || !trade || !gasFee.value) { return () => { - logger.error(new Error('Attempted swap with missing required parameters'), { + logger.error('Attempted swap with missing required parameters', { tags: { file: 'swap/hooks', function: 'useSwapCallback', @@ -802,27 +774,21 @@ export function useSwapCallback( } return () => { - const params: SwapParams = { - txId, - account, - analytics: getBaseTradeAnalyticsProperties({ formatter, trade }), - approveTxRequest, - swapTxRequest, - swapTypeInfo: tradeToTransactionInfo(trade), - } - - appDispatch(swapActions.trigger(params)) + appDispatch( + swapActions.trigger({ + txId, + account, + trade, + currencyInAmountUSD, + currencyOutAmountUSD, + approveTxRequest, + swapTxRequest, + }) + ) onSubmit() - const blockNumber = - trade.quoteData?.quoteType === QuoteType.TradingApi - ? isClassicQuote(trade.quoteData?.quote?.quote) - ? trade.quoteData?.quote?.quote?.blockNumber?.toString() - : undefined - : trade.quoteData?.quote?.blockNumber - - sendWalletAnalyticsEvent(SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED, { - ...getBaseTradeAnalyticsProperties({ formatter, trade }), + sendMobileAnalyticsEvent(SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED, { + ...getBaseTradeAnalyticsProperties(formatter, trade), estimated_network_fee_wei: gasFee.value, gas_limit: toStringish(swapTxRequest.gasLimit), token_in_amount_usd: currencyInAmountUSD @@ -832,7 +798,7 @@ export function useSwapCallback( ? parseFloat(currencyOutAmountUSD.toFixed(2)) : undefined, transaction_deadline_seconds: trade.deadline, - swap_quote_block_number: blockNumber, + swap_quote_block_number: trade.quote?.blockNumber, is_auto_slippage: isAutoSlippage, swap_flow_duration_milliseconds: swapStartTimestamp ? Date.now() - swapStartTimestamp @@ -884,7 +850,7 @@ export function useWrapCallback( if (!isWrapAction(wrapType)) { return { wrapCallback: (): void => - logger.error(new Error('Attempted wrap on a non-wrap transaction'), { + logger.error('Attempted wrap on a non-wrap transaction', { tags: { file: 'swap/hooks', function: 'useWrapCallback', @@ -896,7 +862,7 @@ export function useWrapCallback( if (!account || !inputCurrencyAmount || !txRequest) { return { wrapCallback: (): void => - logger.error(new Error('Attempted wrap with missing required parameters'), { + logger.error('Attempted wrap with missing required parameters', { tags: { file: 'swap/hooks', function: 'useWrapCallback', @@ -908,14 +874,14 @@ export function useWrapCallback( return { wrapCallback: (): void => { - const params: WrapParams = { - account, - inputCurrencyAmount, - txId, - txRequest, - } - - appDispatch(tokenWrapActions.trigger(params)) + appDispatch( + tokenWrapActions.trigger({ + account, + inputCurrencyAmount, + txId, + txRequest, + }) + ) onSuccess() }, } diff --git a/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx b/apps/mobile/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx similarity index 83% rename from packages/wallet/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx rename to apps/mobile/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx index a48b71ee426..25de2305d99 100644 --- a/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx +++ b/apps/mobile/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx @@ -1,10 +1,11 @@ +import React from 'react' import { useTranslation } from 'react-i18next' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' +import { LearnMoreLink } from 'src/components/text/LearnMoreLink' +import { ModalName } from 'src/features/telemetry/constants' import { Icons, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { uniswapUrls } from 'wallet/src/constants/urls' -import { ModalName } from 'wallet/src/telemetry/constants' export function FeeOnTransferInfoModal({ onClose }: { onClose: () => void }): JSX.Element { const { t } = useTranslation() diff --git a/packages/wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx b/apps/mobile/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx similarity index 73% rename from packages/wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx rename to apps/mobile/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx index 129a3288cd3..796af960383 100644 --- a/packages/wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx +++ b/apps/mobile/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx @@ -1,10 +1,11 @@ +import React from 'react' import { useTranslation } from 'react-i18next' +import { WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' +import { LearnMoreLink } from 'src/components/text/LearnMoreLink' +import { ModalName } from 'src/features/telemetry/constants' import { Icons, useSporeColors } from 'ui/src' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { uniswapUrls } from 'wallet/src/constants/urls' -import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' -import { ModalName } from 'wallet/src/telemetry/constants' export function NetworkFeeInfoModal({ onClose }: { onClose: () => void }): JSX.Element { const colors = useSporeColors() diff --git a/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx b/apps/mobile/src/features/transactions/swap/modals/SlippageInfoModal.tsx similarity index 94% rename from packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx rename to apps/mobile/src/features/transactions/swap/modals/SlippageInfoModal.tsx index 54ef2a67ded..8bb7f03142c 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx +++ b/apps/mobile/src/features/transactions/swap/modals/SlippageInfoModal.tsx @@ -1,16 +1,17 @@ import { Currency, TradeType } from '@uniswap/sdk-core' +import React from 'react' import { useTranslation } from 'react-i18next' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' +import { LearnMoreLink } from 'src/components/text/LearnMoreLink' +import { ModalName } from 'src/features/telemetry/constants' +import { slippageToleranceToPercent } from 'src/features/transactions/swap/utils' import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src' import AlertTriangleIcon from 'ui/src/assets/icons/alert-triangle.svg' import { iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' -import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { uniswapUrls } from 'wallet/src/constants/urls' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { Trade } from 'wallet/src/features/transactions/swap/useTrade' -import { slippageToleranceToPercent } from 'wallet/src/features/transactions/swap/utils' -import { ModalName } from 'wallet/src/telemetry/constants' import { getSymbolDisplayText } from 'wallet/src/utils/currency' export type SlippageInfoModalProps = { diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx b/apps/mobile/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx similarity index 82% rename from packages/wallet/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx rename to apps/mobile/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx index aacb60723a0..9d730030530 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx +++ b/apps/mobile/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx @@ -1,10 +1,11 @@ +import React from 'react' import { useTranslation } from 'react-i18next' +import { WarningSeverity } from 'src/components/modals/WarningModal/types' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' +import { ModalName } from 'src/features/telemetry/constants' +import { openUri } from 'src/utils/linking' import { Text, TouchableArea, useSporeColors } from 'ui/src' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { uniswapUrls } from 'wallet/src/constants/urls' -import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' -import { ModalName } from 'wallet/src/telemetry/constants' -import { openUri } from 'wallet/src/utils/linking' export function SwapFeeInfoModal({ onClose, diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx b/apps/mobile/src/features/transactions/swap/modals/SwapProtectionModal.tsx similarity index 79% rename from packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx rename to apps/mobile/src/features/transactions/swap/modals/SwapProtectionModal.tsx index b734148a45f..263d720bdae 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx +++ b/apps/mobile/src/features/transactions/swap/modals/SwapProtectionModal.tsx @@ -1,9 +1,10 @@ +import React from 'react' import { useTranslation } from 'react-i18next' +import WarningModal from 'src/components/modals/WarningModal/WarningModal' +import { LearnMoreLink } from 'src/components/text/LearnMoreLink' +import { ModalName } from 'src/features/telemetry/constants' import { Icons, useSporeColors } from 'ui/src' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { uniswapUrls } from 'wallet/src/constants/urls' -import { ModalName } from 'wallet/src/telemetry/constants' export function SwapProtectionInfoModal({ onClose }: { onClose: () => void }): JSX.Element { const colors = useSporeColors() diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapSettingsModal.tsx b/apps/mobile/src/features/transactions/swap/modals/SwapSettingsModal.tsx similarity index 92% rename from packages/wallet/src/features/transactions/swap/modals/SwapSettingsModal.tsx rename to apps/mobile/src/features/transactions/swap/modals/SwapSettingsModal.tsx index bf681770bf5..4588c350762 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapSettingsModal.tsx +++ b/apps/mobile/src/features/transactions/swap/modals/SwapSettingsModal.tsx @@ -1,23 +1,30 @@ /* eslint-disable max-lines */ +import { BottomSheetTextInput } from '@gorhom/bottom-sheet' import { Trade } from '@uniswap/router-sdk' import { Currency, TradeType } from '@uniswap/sdk-core' import { impactAsync } from 'expo-haptics' -import { useCallback, useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated' -import { isWeb } from 'tamagui' +import { + Easing, + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming, +} from 'react-native-reanimated' +import PlusMinusButton, { PlusMinusButtonType } from 'src/components/buttons/PlusMinusButton' +import { Switch } from 'src/components/buttons/Switch' +import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' +import { LearnMoreLink } from 'src/components/text/LearnMoreLink' +import { ModalName } from 'src/features/telemetry/constants' +import { SwapProtectionInfoModal } from 'src/features/transactions/swap/modals/SwapProtectionModal' +import { DerivedSwapInfo } from 'src/features/transactions/swap/types' +import { slippageToleranceToPercent } from 'src/features/transactions/swap/utils' import { AnimatedFlex, Button, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import AlertTriangleIcon from 'ui/src/assets/icons/alert-triangle.svg' import { fonts, iconSizes, spacing } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' -import PlusMinusButton, { PlusMinusButtonType } from 'wallet/src/components/buttons/PlusMinusButton' -import { Switch } from 'wallet/src/components/buttons/Switch' -import { - BottomSheetModal, - BottomSheetTextInput, -} from 'wallet/src/components/modals/BottomSheetModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' -import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' import { MAX_AUTO_SLIPPAGE_TOLERANCE, MAX_CUSTOM_SLIPPAGE_TOLERANCE, @@ -27,14 +34,9 @@ import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { isPrivateRpcSupportedOnChain } from 'wallet/src/features/providers' -import { SwapProtectionInfoModal } from 'wallet/src/features/transactions/swap/modals/SwapProtectionModal' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { slippageToleranceToPercent } from 'wallet/src/features/transactions/swap/utils' import { useSwapProtectionSetting } from 'wallet/src/features/wallet/hooks' -import { SwapProtectionSetting, setSwapProtectionSetting } from 'wallet/src/features/wallet/slice' +import { setSwapProtectionSetting, SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' -import { errorShakeAnimation } from 'wallet/src/utils/animations' import { getSymbolDisplayText } from 'wallet/src/utils/currency' const SLIPPAGE_INCREMENT = 0.1 @@ -268,12 +270,9 @@ function SlippageSettings({ const showSlippageWarning = parsedInputSlippageTolerance > autoSlippageTolerance const inputShakeX = useSharedValue(0) - const inputAnimatedStyle = useAnimatedStyle( - () => ({ - transform: [{ translateX: inputShakeX.value }], - }), - [inputShakeX] - ) + const inputAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: inputShakeX.value }], + })) const onPressAutoSlippage = (): void => { setAutoSlippageEnabled(true) @@ -324,7 +323,14 @@ function SlippageSettings({ * without the input shaking (ex. typing 0.x shouldn't shake after typing char) */ if (isInvalidNumber || overMaxTolerance || moreThanOneDecimalSymbol || moreThanTwoDecimals) { - inputShakeX.value = errorShakeAnimation(inputShakeX) + inputShakeX.value = withRepeat( + withTiming(5, { duration: 50, easing: Easing.inOut(Easing.ease) }), + 3, + true, + () => { + inputShakeX.value = 0 + } + ) await impactAsync() return } @@ -417,11 +423,9 @@ function SlippageSettings({ style={{ color: autoSlippageEnabled ? colors.neutral2.get() : colors.neutral1.get(), fontSize: fonts.subheading1.fontSize, + fontFamily: fonts.subheading1.family, width: fonts.subheading1.fontSize * 4, padding: spacing.none, - ...(!isWeb && { - fontFamily: fonts.subheading1.family, - }), }} textAlign="center" value={ diff --git a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts b/apps/mobile/src/features/transactions/swap/swapSaga.test.ts similarity index 87% rename from packages/wallet/src/features/transactions/swap/swapSaga.test.ts rename to apps/mobile/src/features/transactions/swap/swapSaga.test.ts index 50140811d89..a9f1177f74f 100644 --- a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts +++ b/apps/mobile/src/features/transactions/swap/swapSaga.test.ts @@ -5,12 +5,11 @@ import { TradeType } from '@uniswap/sdk-core' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { expectSaga } from 'redux-saga-test-plan' import { EffectProviders, StaticProvider } from 'redux-saga-test-plan/providers' +import { approveAndSwap, SwapParams } from 'src/features/transactions/swap/swapSaga' import { ChainId } from 'wallet/src/constants/chains' import { DAI } from 'wallet/src/constants/tokens' import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' -import { getBaseTradeAnalyticsProperties } from 'wallet/src/features/transactions/swap/analytics' -import { SwapParams, approveAndSwap } from 'wallet/src/features/transactions/swap/swapSaga' import { Trade } from 'wallet/src/features/transactions/swap/useTrade' import { ExactInputSwapTransactionInfo, @@ -56,19 +55,17 @@ const mockSwapTxRequest = { const swapParams: SwapParams = { txId: '1', account, - analytics: {} as ReturnType, + trade: mockTrade, approveTxRequest: mockApproveTxRequest, swapTxRequest: mockSwapTxRequest, - swapTypeInfo: transactionTypeInfo, } const swapParamsWithoutApprove: SwapParams = { txId: '1', account, - analytics: {} as ReturnType, + trade: mockTrade, approveTxRequest: undefined, swapTxRequest: mockSwapTxRequest, - swapTypeInfo: transactionTypeInfo, } const nonce = 1 @@ -83,7 +80,6 @@ describe(approveAndSwap, () => { account: swapParams.account, options: { request: mockApproveTxRequest }, typeInfo: transactionTypeInfo, - analytics: swapParams.analytics, }), undefined, ], @@ -103,7 +99,6 @@ describe(approveAndSwap, () => { account: swapParams.account, options: { request: { ...mockSwapTxRequest, nonce: nonce + 1 } }, typeInfo: transactionTypeInfo, - analytics: swapParams.analytics, }), undefined, ], diff --git a/packages/wallet/src/features/transactions/swap/swapSaga.ts b/apps/mobile/src/features/transactions/swap/swapSaga.ts similarity index 77% rename from packages/wallet/src/features/transactions/swap/swapSaga.ts rename to apps/mobile/src/features/transactions/swap/swapSaga.ts index 4781f3dd1dc..a011738ffc5 100644 --- a/packages/wallet/src/features/transactions/swap/swapSaga.ts +++ b/apps/mobile/src/features/transactions/swap/swapSaga.ts @@ -1,18 +1,14 @@ import { providers } from 'ethers' +import { tradeToTransactionInfo } from 'src/features/transactions/swap/utils' import { Statsig } from 'statsig-react-native' -import { isWeb } from 'tamagui' import { call, select } from 'typed-redux-saga' import { logger } from 'utilities/src/logger/logger' import { RPCType } from 'wallet/src/constants/chains' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { isPrivateRpcSupportedOnChain } from 'wallet/src/features/providers' import { makeSelectAddressTransactions } from 'wallet/src/features/transactions/selectors' -import { - SendTransactionParams, - sendTransaction, -} from 'wallet/src/features/transactions/sendTransactionSaga' -import { getBaseTradeAnalyticsProperties } from 'wallet/src/features/transactions/swap/analytics' -import { tradeToTransactionInfo } from 'wallet/src/features/transactions/swap/utils' +import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' +import { Trade } from 'wallet/src/features/transactions/swap/useTrade' import { TransactionStatus, TransactionType, @@ -27,15 +23,14 @@ import { createMonitoredSaga } from 'wallet/src/utils/saga' export type SwapParams = { txId?: string account: Account - analytics: ReturnType + trade: Trade approveTxRequest?: providers.TransactionRequest swapTxRequest: providers.TransactionRequest - swapTypeInfo: ReturnType } export function* approveAndSwap(params: SwapParams) { try { - const { account, approveTxRequest, swapTxRequest, txId, analytics, swapTypeInfo } = params + const { account, approveTxRequest, swapTxRequest, txId, trade } = params if (!swapTxRequest.chainId || !swapTxRequest.to || (approveTxRequest && !approveTxRequest.to)) { throw new Error('approveAndSwap received incomplete transaction request details') } @@ -55,15 +50,13 @@ export function* approveAndSwap(params: SwapParams) { spender: swapTxRequest.to, } - const sendTransactionParams: SendTransactionParams = { + yield* call(sendTransaction, { chainId, account, options: { request: approveTxRequest, submitViaPrivateRpc }, typeInfo, - analytics, - } - - yield* call(sendTransaction, sendTransactionParams) + trade, + }) } const request = { @@ -71,21 +64,18 @@ export function* approveAndSwap(params: SwapParams) { nonce: approveTxRequest ? nonce + 1 : undefined, } - const sendTransactionParams: SendTransactionParams = { + const swapTypeInfo = tradeToTransactionInfo(trade) + + yield* call(sendTransaction, { txId, chainId, account, options: { request, submitViaPrivateRpc }, typeInfo: swapTypeInfo, - analytics, - } - - yield* call(sendTransaction, sendTransactionParams) - } catch (error) { - logger.error(error, { - tags: { file: 'swapSaga', function: 'approveAndSwap' }, - extra: { analytics: params.analytics }, + trade, }) + } catch (error) { + logger.error(error, { tags: { file: 'swapSaga', function: 'approveAndSwap' } }) } } @@ -118,8 +108,7 @@ function* getNonceForApproveAndSwap( function* shouldSubmitViaPrivateRpc(chainId: number) { const swapProtectionSetting = yield* select(selectWalletSwapProtectionSetting) const swapProtectionOn = swapProtectionSetting === SwapProtectionSetting.On - // TODO(EXT-460): remove this once Statsig is set up in the Extension - const mevBlockerFeatureEnabled = isWeb ? true : Statsig.checkGate(FEATURE_FLAGS.MevBlocker) + const mevBlockerFeatureEnabled = Statsig.checkGate(FEATURE_FLAGS.MevBlocker) const privateRpcSupportedOnChain = chainId ? isPrivateRpcSupportedOnChain(chainId) : false return Boolean(swapProtectionOn && privateRpcSupportedOnChain && mevBlockerFeatureEnabled) } diff --git a/packages/wallet/src/features/transactions/swap/types.ts b/apps/mobile/src/features/transactions/swap/types.ts similarity index 89% rename from packages/wallet/src/features/transactions/swap/types.ts rename to apps/mobile/src/features/transactions/swap/types.ts index ce9c257d3d9..43898638546 100644 --- a/packages/wallet/src/features/transactions/swap/types.ts +++ b/apps/mobile/src/features/transactions/swap/types.ts @@ -1,9 +1,9 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { CurrencyInfo } from 'src/features/dataApi/types' +import { BaseDerivedInfo } from 'src/features/transactions/transactionState/types' import { ChainId } from 'wallet/src/constants/chains' -import { CurrencyInfo } from 'wallet/src/features/dataApi/types' import { useTrade } from 'wallet/src/features/transactions/swap/useTrade' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { BaseDerivedInfo } from 'wallet/src/features/transactions/transfer/types' import { WrapType } from 'wallet/src/features/transactions/types' export type DerivedSwapInfo< diff --git a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts b/apps/mobile/src/features/transactions/swap/useSwapWarnings.test.ts similarity index 95% rename from packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts rename to apps/mobile/src/features/transactions/swap/useSwapWarnings.test.ts index efeb935c9ee..cb65217f81b 100644 --- a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts +++ b/apps/mobile/src/features/transactions/swap/useSwapWarnings.test.ts @@ -1,13 +1,12 @@ import { CurrencyAmount } from '@uniswap/sdk-core' +import { WarningLabel } from 'src/components/modals/WarningModal/types' +import { getSwapWarnings } from 'src/features/transactions/swap/useSwapWarnings' import { ChainId } from 'wallet/src/constants/chains' import { DAI, USDC } from 'wallet/src/constants/tokens' import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { getSwapWarnings } from 'wallet/src/features/transactions/hooks/useSwapWarnings' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { WrapType } from 'wallet/src/features/transactions/types' import { isOffline } from 'wallet/src/features/transactions/utils' -import { WarningLabel } from 'wallet/src/features/transactions/WarningModal/types' import { daiCurrencyInfo, ethCurrencyInfo, @@ -15,6 +14,7 @@ import { networkUnknown, networkUp, } from 'wallet/src/test/fixtures' +import { DerivedSwapInfo } from './types' const ETH = NativeCurrency.onChain(ChainId.Mainnet) diff --git a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx b/apps/mobile/src/features/transactions/swap/useSwapWarnings.tsx similarity index 96% rename from packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx rename to apps/mobile/src/features/transactions/swap/useSwapWarnings.tsx index c0ccbd9eeff..db3a99af4c9 100644 --- a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx +++ b/apps/mobile/src/features/transactions/swap/useSwapWarnings.tsx @@ -2,6 +2,14 @@ import { useNetInfo } from '@react-native-community/netinfo' import { Percent } from '@uniswap/sdk-core' import _ from 'lodash' import { TFunction } from 'react-i18next' +import { getNetworkWarning } from 'src/components/modals/WarningModal/constants' +import { + Warning, + WarningAction, + WarningLabel, + WarningSeverity, +} from 'src/components/modals/WarningModal/types' +import { DerivedSwapInfo } from 'src/features/transactions/swap/types' import { formatPriceImpact } from 'utilities/src/format/formatPriceImpact' import { useMemoCompare } from 'utilities/src/react/hooks' import { useSwapRewriteEnabled } from 'wallet/src/features/experiments/hooks' @@ -10,16 +18,8 @@ import { NO_QUOTE_DATA, SWAP_QUOTE_ERROR, } from 'wallet/src/features/routing/api' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { isOffline } from 'wallet/src/features/transactions/utils' -import { getNetworkWarning } from 'wallet/src/features/transactions/WarningModal/getNetworkWarning' -import { - Warning, - WarningAction, - WarningLabel, - WarningSeverity, -} from 'wallet/src/features/transactions/WarningModal/types' const PRICE_IMPACT_THRESHOLD_MEDIUM = new Percent(3, 100) // 3% const PRICE_IMPACT_THRESHOLD_HIGH = new Percent(5, 100) // 5% diff --git a/apps/mobile/src/features/transactions/swap/utils.test.ts b/apps/mobile/src/features/transactions/swap/utils.test.ts new file mode 100644 index 00000000000..28bdf7fa329 --- /dev/null +++ b/apps/mobile/src/features/transactions/swap/utils.test.ts @@ -0,0 +1,108 @@ +import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { Route } from '@uniswap/v3-sdk' +import { getWrapType, requireAcceptNewTrade } from 'src/features/transactions/swap/utils' +import { ChainId } from 'wallet/src/constants/chains' +import { UNI, WBTC, wrappedNativeCurrency } from 'wallet/src/constants/tokens' +import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { Trade } from 'wallet/src/features/transactions/swap/useTrade' +import { WrapType } from 'wallet/src/features/transactions/types' +import { mockPool } from 'wallet/src/test/fixtures' + +describe(getWrapType, () => { + const eth = NativeCurrency.onChain(ChainId.Mainnet) + const weth = wrappedNativeCurrency(ChainId.Mainnet) + + const goerliEth = NativeCurrency.onChain(ChainId.Goerli) + const goerliWeth = wrappedNativeCurrency(ChainId.Goerli) + + it('handles undefined args', () => { + expect(getWrapType(undefined, weth)).toEqual(WrapType.NotApplicable) + expect(getWrapType(weth, undefined)).toEqual(WrapType.NotApplicable) + expect(getWrapType(undefined, undefined)).toEqual(WrapType.NotApplicable) + }) + + it('handles wrap', () => { + expect(getWrapType(eth, weth)).toEqual(WrapType.Wrap) + + // different chains + expect(getWrapType(goerliEth, weth)).toEqual(WrapType.NotApplicable) + expect(getWrapType(eth, goerliWeth)).toEqual(WrapType.NotApplicable) + }) + + it('handles unwrap', () => { + expect(getWrapType(weth, eth)).toEqual(WrapType.Unwrap) + + // different chains + expect(getWrapType(weth, goerliEth)).toEqual(WrapType.NotApplicable) + expect(getWrapType(goerliWeth, eth)).toEqual(WrapType.NotApplicable) + }) +}) + +describe(requireAcceptNewTrade, () => { + const oldTrade = new Trade({ + v3Routes: [ + { + routev3: new Route([mockPool], UNI[ChainId.Mainnet], WBTC), + inputAmount: CurrencyAmount.fromRawAmount(UNI[ChainId.Mainnet], 1000), + outputAmount: CurrencyAmount.fromRawAmount(WBTC, 1000), + }, + ], + v2Routes: [], + mixedRoutes: [], + tradeType: TradeType.EXACT_INPUT, + slippageTolerance: 0.5, + }) + + it('returns false when prices are within threshold', () => { + const newTrade = new Trade({ + v3Routes: [ + { + routev3: new Route([mockPool], UNI[ChainId.Mainnet], WBTC), + inputAmount: CurrencyAmount.fromRawAmount(UNI[ChainId.Mainnet], 1000), + // Update this number if `ACCEPT_NEW_TRADE_THRESHOLD` changes + outputAmount: CurrencyAmount.fromRawAmount(WBTC, 990), + }, + ], + v2Routes: [], + mixedRoutes: [], + tradeType: TradeType.EXACT_INPUT, + slippageTolerance: 0.5, + }) + expect(requireAcceptNewTrade(oldTrade, newTrade)).toBe(false) + }) + + it('returns true when prices move above threshold', () => { + const newTrade = new Trade({ + v3Routes: [ + { + routev3: new Route([mockPool], UNI[ChainId.Mainnet], WBTC), + inputAmount: CurrencyAmount.fromRawAmount(UNI[ChainId.Mainnet], 1000), + // Update this number if `ACCEPT_NEW_TRADE_THRESHOLD` changes + outputAmount: CurrencyAmount.fromRawAmount(WBTC, 979), + }, + ], + v2Routes: [], + mixedRoutes: [], + tradeType: TradeType.EXACT_INPUT, + slippageTolerance: 0.5, + }) + expect(requireAcceptNewTrade(oldTrade, newTrade)).toBe(true) + }) + + it('returns false when new price is better', () => { + const newTrade = new Trade({ + v3Routes: [ + { + routev3: new Route([mockPool], UNI[ChainId.Mainnet], WBTC), + inputAmount: CurrencyAmount.fromRawAmount(UNI[ChainId.Mainnet], 1000), + outputAmount: CurrencyAmount.fromRawAmount(WBTC, 2000000), + }, + ], + v2Routes: [], + mixedRoutes: [], + tradeType: TradeType.EXACT_INPUT, + slippageTolerance: 0.5, + }) + expect(requireAcceptNewTrade(oldTrade, newTrade)).toBe(false) + }) +}) diff --git a/apps/mobile/src/features/transactions/swap/utils.ts b/apps/mobile/src/features/transactions/swap/utils.ts new file mode 100644 index 00000000000..aa929ca9c89 --- /dev/null +++ b/apps/mobile/src/features/transactions/swap/utils.ts @@ -0,0 +1,238 @@ +import { Protocol } from '@uniswap/router-sdk' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { + FlatFeeOptions, + SwapOptions as UniversalRouterSwapOptions, + SwapRouter as UniversalSwapRouter, +} from '@uniswap/universal-router-sdk' +import { FeeOptions } from '@uniswap/v3-sdk' +import { BigNumber } from 'ethers' +import { ElementName } from 'src/features/telemetry/constants' +import { AppTFunction } from 'ui/src/i18n/types' +import { NumberType } from 'utilities/src/format/types' +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { LocalizationContextState } from 'wallet/src/features/language/LocalizationContext' +import { PermitSignatureInfo } from 'wallet/src/features/transactions/swap/usePermit2Signature' +import { Trade } from 'wallet/src/features/transactions/swap/useTrade' +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' +import { + ExactInputSwapTransactionInfo, + ExactOutputSwapTransactionInfo, + TransactionType, + WrapType, +} from 'wallet/src/features/transactions/types' +import { getSymbolDisplayText } from 'wallet/src/utils/currency' +import { + areCurrencyIdsEqual, + buildWrappedNativeCurrencyId, + CurrencyId, + currencyId, + currencyIdToAddress, + currencyIdToChain, +} from 'wallet/src/utils/currencyId' + +export function getWrapType( + inputCurrency: Currency | null | undefined, + outputCurrency: Currency | null | undefined +): WrapType { + if (!inputCurrency || !outputCurrency || inputCurrency.chainId !== outputCurrency.chainId) { + return WrapType.NotApplicable + } + + const inputChainId = inputCurrency.chainId as ChainId + const wrappedCurrencyId = buildWrappedNativeCurrencyId(inputChainId) + + if ( + inputCurrency.isNative && + areCurrencyIdsEqual(currencyId(outputCurrency), wrappedCurrencyId) + ) { + return WrapType.Wrap + } else if ( + outputCurrency.isNative && + areCurrencyIdsEqual(currencyId(inputCurrency), wrappedCurrencyId) + ) { + return WrapType.Unwrap + } + + return WrapType.NotApplicable +} + +export function isWrapAction(wrapType: WrapType): wrapType is WrapType.Unwrap | WrapType.Wrap { + return wrapType === WrapType.Unwrap || wrapType === WrapType.Wrap +} + +export function tradeToTransactionInfo( + trade: Trade +): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo { + const slippageTolerancePercent = slippageToleranceToPercent(trade.slippageTolerance) + const { quote, slippageTolerance } = trade + const { gasUseEstimate, quoteId, routeString } = quote || {} + + const baseTransactionInfo = { + inputCurrencyId: currencyId(trade.inputAmount.currency), + outputCurrencyId: currencyId(trade.outputAmount.currency), + slippageTolerance, + quoteId, + gasUseEstimate, + routeString, + protocol: getProtocolVersionFromTrade(trade), + } + + return trade.tradeType === TradeType.EXACT_INPUT + ? { + ...baseTransactionInfo, + type: TransactionType.Swap, + tradeType: TradeType.EXACT_INPUT, + inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(), + expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(), + minimumOutputCurrencyAmountRaw: trade + .minimumAmountOut(slippageTolerancePercent) + .quotient.toString(), + } + : { + ...baseTransactionInfo, + type: TransactionType.Swap, + tradeType: TradeType.EXACT_OUTPUT, + outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(), + expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(), + maximumInputCurrencyAmountRaw: trade + .maximumAmountIn(slippageTolerancePercent) + .quotient.toString(), + } +} + +// any price movement below ACCEPT_NEW_TRADE_THRESHOLD is auto-accepted for the user +const ACCEPT_NEW_TRADE_THRESHOLD = new Percent(1, 100) +export function requireAcceptNewTrade(oldTrade: Maybe, newTrade: Maybe): boolean { + if (!oldTrade || !newTrade) { + return false + } + + return ( + oldTrade.tradeType !== newTrade.tradeType || + !oldTrade.inputAmount.currency.equals(newTrade.inputAmount.currency) || + !oldTrade.outputAmount.currency.equals(newTrade.outputAmount.currency) || + newTrade.executionPrice.lessThan(oldTrade.worstExecutionPrice(ACCEPT_NEW_TRADE_THRESHOLD)) + ) +} + +export const getRateToDisplay = ( + formatter: LocalizationContextState, + trade: Trade, + showInverseRate: boolean +): string => { + const price = showInverseRate ? trade.executionPrice.invert() : trade.executionPrice + const formattedPrice = formatter.formatNumberOrString({ + value: price.toSignificant(), + type: NumberType.SwapPrice, + }) + const quoteCurrencySymbol = getSymbolDisplayText(trade.executionPrice.quoteCurrency.symbol) + const baseCurrencySymbol = getSymbolDisplayText(trade.executionPrice.baseCurrency.symbol) + const rate = `1 ${quoteCurrencySymbol} = ${formattedPrice} ${baseCurrencySymbol}` + const inverseRate = `1 ${baseCurrencySymbol} = ${formattedPrice} ${quoteCurrencySymbol}` + return showInverseRate ? rate : inverseRate +} + +export const getActionName = (t: AppTFunction, wrapType: WrapType): string => { + switch (wrapType) { + case WrapType.Unwrap: + return t('Unwrap') + case WrapType.Wrap: + return t('Wrap') + default: + return t('Swap') + } +} + +export const getActionElementName = (wrapType: WrapType): ElementName => { + switch (wrapType) { + case WrapType.Unwrap: + return ElementName.Unwrap + case WrapType.Wrap: + return ElementName.Wrap + default: + return ElementName.Swap + } +} + +export function sumGasFees(gasFee1?: string | undefined, gasFee2?: string): string | undefined { + if (!gasFee1 || !gasFee2) { + return gasFee1 || gasFee2 + } + + return BigNumber.from(gasFee1).add(gasFee2).toString() +} + +export const prepareSwapFormState = ({ + inputCurrencyId, +}: { + inputCurrencyId?: CurrencyId +}): TransactionState | undefined => { + return inputCurrencyId + ? { + exactCurrencyField: CurrencyField.INPUT, + exactAmountToken: '', + [CurrencyField.INPUT]: { + address: currencyIdToAddress(inputCurrencyId), + chainId: currencyIdToChain(inputCurrencyId) ?? ChainId.Mainnet, + type: AssetType.Currency, + }, + [CurrencyField.OUTPUT]: null, + } + : undefined +} + +// rounds to nearest basis point +export const slippageToleranceToPercent = (slippage: number): Percent => { + const basisPoints = Math.round(slippage * 100) + return new Percent(basisPoints, 10_000) +} + +interface MethodParameterArgs { + permit2Signature?: PermitSignatureInfo + trade: Trade + address: string + feeOptions?: FeeOptions + flatFeeOptions?: FlatFeeOptions +} + +export const getSwapMethodParameters = ({ + permit2Signature, + trade, + address, + feeOptions, + flatFeeOptions, +}: MethodParameterArgs): { calldata: string; value: string } => { + const slippageTolerancePercent = slippageToleranceToPercent(trade.slippageTolerance) + const baseOptions = { + slippageTolerance: slippageTolerancePercent, + recipient: address, + fee: feeOptions, + flatFee: flatFeeOptions, + } + + const universalRouterSwapOptions: UniversalRouterSwapOptions = permit2Signature + ? { + ...baseOptions, + inputTokenPermit: { + signature: permit2Signature.signature, + ...permit2Signature.permitMessage, + }, + } + : baseOptions + return UniversalSwapRouter.swapERC20CallParameters(trade, universalRouterSwapOptions) +} + +export function getProtocolVersionFromTrade(trade: Trade): Protocol { + if (trade.routes.every((r) => r.protocol === Protocol.V2)) { + return Protocol.V2 + } + if (trade.routes.every((r) => r.protocol === Protocol.V3)) { + return Protocol.V3 + } + return Protocol.MIXED +} diff --git a/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts b/apps/mobile/src/features/transactions/swap/wrapSaga.test.ts similarity index 92% rename from packages/wallet/src/features/transactions/swap/wrapSaga.test.ts rename to apps/mobile/src/features/transactions/swap/wrapSaga.test.ts index cbfac52cc1f..04021f5b2db 100644 --- a/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts +++ b/apps/mobile/src/features/transactions/swap/wrapSaga.test.ts @@ -1,9 +1,9 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import { testSaga } from 'redux-saga-test-plan' +import { Params, wrap } from 'src/features/transactions/swap/wrapSaga' import { ChainId } from 'wallet/src/constants/chains' import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' -import { wrap, WrapParams } from 'wallet/src/features/transactions/swap/wrapSaga' import { TransactionType, WrapTransactionInfo } from 'wallet/src/features/transactions/types' import { account } from 'wallet/src/test/fixtures' @@ -25,7 +25,7 @@ const transaction = { chainId: ChainId.Mainnet, } -const params: WrapParams = { +const params: Params = { txId: '1', account, txRequest: transaction, @@ -51,7 +51,7 @@ describe(wrap, () => { }) it('successfully unwraps to native eth', () => { - const unwrapParams: WrapParams = { + const unwrapParams: Params = { ...params, inputCurrencyAmount: CurrencyAmount.fromRawAmount( NativeCurrency.onChain(ChainId.Mainnet).wrapped, diff --git a/packages/wallet/src/features/transactions/swap/wrapSaga.ts b/apps/mobile/src/features/transactions/swap/wrapSaga.ts similarity index 94% rename from packages/wallet/src/features/transactions/swap/wrapSaga.ts rename to apps/mobile/src/features/transactions/swap/wrapSaga.ts index ce90b32a092..0c65268e12e 100644 --- a/packages/wallet/src/features/transactions/swap/wrapSaga.ts +++ b/apps/mobile/src/features/transactions/swap/wrapSaga.ts @@ -15,7 +15,7 @@ import { import { Account } from 'wallet/src/features/wallet/accounts/types' import { createMonitoredSaga } from 'wallet/src/utils/saga' -export type WrapParams = { +export type Params = { txId?: string txRequest: providers.TransactionRequest account: Account @@ -29,7 +29,7 @@ export async function getWethContract( return new Contract(getWrappedNativeAddress(chainId), WETH_ABI, provider) as Weth } -export function* wrap(params: WrapParams) { +export function* wrap(params: Params) { try { const { account, inputCurrencyAmount, txRequest, txId } = params let typeInfo: TransactionTypeInfo @@ -69,4 +69,4 @@ export const { wrappedSaga: tokenWrapSaga, reducer: tokenWrapReducer, actions: tokenWrapActions, -} = createMonitoredSaga(wrap, 'wrap') +} = createMonitoredSaga(wrap, 'wrap') diff --git a/packages/wallet/src/features/transactions/swap/CurrencyInputPanel.tsx b/apps/mobile/src/features/transactions/swapRewrite/CurrencyInputPanel.tsx similarity index 93% rename from packages/wallet/src/features/transactions/swap/CurrencyInputPanel.tsx rename to apps/mobile/src/features/transactions/swapRewrite/CurrencyInputPanel.tsx index de0cc9774e5..ae5714e01d8 100644 --- a/packages/wallet/src/features/transactions/swap/CurrencyInputPanel.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/CurrencyInputPanel.tsx @@ -6,10 +6,8 @@ import { TextInput, TextInputProps, TextInputSelectionChangeEventData, - ViewStyle, } from 'react-native' import { - AnimatedStyleProp, Easing, useAnimatedStyle, useSharedValue, @@ -17,19 +15,19 @@ import { withSequence, withTiming, } from 'react-native-reanimated' +import { useDynamicFontSizing } from 'src/app/hooks' +import { AmountInput } from 'src/components/input/AmountInput' +import { SelectTokenButton } from 'src/components/TokenSelector/SelectTokenButton' +import { MaxAmountButton } from 'src/features/transactions/swapRewrite/MaxAmountButton' import { AnimatedFlex, Flex, FlexProps, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import { fonts, spacing } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' import { useForwardRef, usePrevious } from 'utilities/src/react/hooks' -import { AmountInput } from 'wallet/src/components/input/AmountInput' -import { SelectTokenButton } from 'wallet/src/components/TokenSelector/SelectTokenButton' import { CurrencyInfo } from 'wallet/src/features/dataApi/types' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { MaxAmountButton } from 'wallet/src/features/transactions/swap/MaxAmountButton' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { getSymbolDisplayText } from 'wallet/src/utils/currency' -import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' type CurrentInputPanelProps = { autoFocus?: boolean @@ -46,7 +44,7 @@ type CurrentInputPanelProps = { onSetExactAmount: (amount: string) => void onSetMax?: (amount: string, currencyField: CurrencyField) => void onShowTokenSelector: () => void - onToggleIsFiatMode: (currencyField: CurrencyField) => void + onToggleIsFiatMode: () => void selection?: TextInputProps['selection'] showNonZeroBalancesOnly?: boolean showSoftInputOnFocus?: boolean @@ -109,11 +107,6 @@ export const CurrencyInputPanel = memo( usdValue?.toExact(), NumberType.FiatTokenQuantity ) - - const _onToggleIsFiatMode = useCallback(() => { - onToggleIsFiatMode(currencyField) - }, [currencyField, onToggleIsFiatMode]) - const formattedCurrencyAmount = currencyAmount ? formatCurrencyAmount({ value: currencyAmount, type: NumberType.TokenTx }) : '' @@ -227,7 +220,12 @@ export const CurrencyInputPanel = memo( return ( - + + style={animatedInfoRowStyle} + width="100%"> {/* Keep the animated parent container so animation styles are always mounted */} {currencyInfo && ( <> - + {inputPanelFormattedValue} - {Boolean(inputPanelFormattedValue && fiatModeFeatureEnabled) && ( + {inputPanelFormattedValue && fiatModeFeatureEnabled && ( )} @@ -355,9 +353,16 @@ function useAnimatedContainerStyles( isLoading: boolean | undefined, isCollapsed: boolean | undefined ): { - animatedContainerStyle: AnimatedStyleProp - animatedAmountInputStyle: AnimatedStyleProp - animatedInfoRowStyle: AnimatedStyleProp + animatedContainerStyle: { + paddingTop: number + paddingBottom: number + } + animatedAmountInputStyle: { + opacity: number + } + animatedInfoRowStyle: { + bottom: number + } } { const animatedContainerStyle = useAnimatedStyle(() => { return { @@ -379,7 +384,6 @@ function useAnimatedContainerStyles( -1, true ) - const animatedAmountInputStyle = useAnimatedStyle( () => ({ opacity: isLoading ? loadingFlexProgress.value : 1, diff --git a/packages/wallet/src/features/transactions/swap/DecimalPad.tsx b/apps/mobile/src/features/transactions/swapRewrite/DecimalPad.tsx similarity index 98% rename from packages/wallet/src/features/transactions/swap/DecimalPad.tsx rename to apps/mobile/src/features/transactions/swapRewrite/DecimalPad.tsx index 11289707871..34430408d01 100644 --- a/packages/wallet/src/features/transactions/swap/DecimalPad.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/DecimalPad.tsx @@ -1,5 +1,5 @@ import { ImpactFeedbackStyle } from 'expo-haptics' -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { I18nManager, LayoutChangeEvent } from 'react-native' import { Flex, Icons, Text, TouchableArea, useMedia } from 'ui/src' import { fonts } from 'ui/src/theme' diff --git a/packages/wallet/src/features/transactions/swap/DecimalPadInput.tsx b/apps/mobile/src/features/transactions/swapRewrite/DecimalPadInput.tsx similarity index 96% rename from packages/wallet/src/features/transactions/swap/DecimalPadInput.tsx rename to apps/mobile/src/features/transactions/swapRewrite/DecimalPadInput.tsx index 1e8b5c1a4b9..32b7df2a95a 100644 --- a/packages/wallet/src/features/transactions/swap/DecimalPadInput.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/DecimalPadInput.tsx @@ -7,8 +7,8 @@ import { useMemo, useState, } from 'react' -import { TextInputProps } from 'wallet/src/components/input/TextInput' -import { DecimalPad, KeyAction, KeyLabel } from './DecimalPad' +import { TextInputProps } from 'src/components/input/TextInput' +import { DecimalPad, KeyAction, KeyLabel } from 'src/features/transactions/swapRewrite/DecimalPad' type DisableKeyCondition = (value: string) => boolean @@ -82,7 +82,6 @@ export const DecimalPadInput = memo( // Prevent unnecessary re-renders and return the same value // if no key was updated (react state won't be updated if value is the // same as the previous one in terms of referential equality) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isUpdated ? newDisabledKeys : prevDisabledKeys }) }, diff --git a/packages/wallet/src/features/transactions/swap/GasAndWarningRows.tsx b/apps/mobile/src/features/transactions/swapRewrite/GasAndWarningRows.tsx similarity index 86% rename from packages/wallet/src/features/transactions/swap/GasAndWarningRows.tsx rename to apps/mobile/src/features/transactions/swapRewrite/GasAndWarningRows.tsx index 0cd3d538f52..ca890ef73de 100644 --- a/packages/wallet/src/features/transactions/swap/GasAndWarningRows.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/GasAndWarningRows.tsx @@ -1,7 +1,12 @@ import { useCallback, useState } from 'react' import { Keyboard } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { isWeb } from 'tamagui' +import { NetworkFeeInfoModal } from 'src/features/transactions/swap/modals/NetworkFeeInfoModal' +import { useSwapFormContext } from 'src/features/transactions/swapRewrite/contexts/SwapFormContext' +import { useSwapTxContext } from 'src/features/transactions/swapRewrite/contexts/SwapTxContext' +import { useParsedSwapWarnings } from 'src/features/transactions/swapRewrite/hooks/useParsedSwapWarnings' +import { SwapWarningModal } from 'src/features/transactions/swapRewrite/SwapWarningModal' +import { BlockedAddressWarning } from 'src/features/trm/BlockedAddressWarning' import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' @@ -9,12 +14,6 @@ import { SwapRewriteVariant } from 'wallet/src/features/experiments/constants' import { useSwapRewriteVariant } from 'wallet/src/features/experiments/hooks' import { useUSDValue } from 'wallet/src/features/gas/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' -import { useSwapTxContext } from 'wallet/src/features/transactions/contexts/SwapTxContext' -import { useParsedSwapWarnings } from 'wallet/src/features/transactions/hooks/useParsedSwapWarnings' -import { NetworkFeeInfoModal } from 'wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal' -import { SwapWarningModal } from 'wallet/src/features/transactions/swap/SwapWarningModal' -import { BlockedAddressWarning } from 'wallet/src/features/trm/BlockedAddressWarning' import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' export function GasAndWarningRows({ renderEmptyRows }: { renderEmptyRows: boolean }): JSX.Element { @@ -100,8 +99,7 @@ export function GasAndWarningRows({ renderEmptyRows }: { renderEmptyRows: boolea centered row entering={FadeIn} - // TODO(EXT-526): re-enable `exiting` animation when it's fixed. - exiting={isWeb ? undefined : FadeOut} + exiting={FadeOut} gap="$spacing8" px="$spacing24"> {formScreenWarning.Icon && ( diff --git a/packages/wallet/src/features/transactions/swap/HoldToSwapProgressCircle.tsx b/apps/mobile/src/features/transactions/swapRewrite/HoldToSwapProgressCircle.tsx similarity index 92% rename from packages/wallet/src/features/transactions/swap/HoldToSwapProgressCircle.tsx rename to apps/mobile/src/features/transactions/swapRewrite/HoldToSwapProgressCircle.tsx index 29dd1fac77b..0b7343a9ebe 100644 --- a/packages/wallet/src/features/transactions/swap/HoldToSwapProgressCircle.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/HoldToSwapProgressCircle.tsx @@ -2,11 +2,11 @@ import { impactAsync, ImpactFeedbackStyle } from 'expo-haptics' import { useEffect, useRef } from 'react' import Animated, { useAnimatedProps, useSharedValue, withTiming } from 'react-native-reanimated' import Svg, { Circle } from 'react-native-svg' -import { iconSizes } from 'ui/src/theme' import { SwapScreen, useSwapScreenContext, -} from 'wallet/src/features/transactions/contexts/SwapScreenContext' +} from 'src/features/transactions/swapRewrite/contexts/SwapScreenContext' +import { iconSizes } from 'ui/src/theme' export const HOLD_TO_SWAP_TIMEOUT = 3000 @@ -38,12 +38,9 @@ export function HoldToSwapProgressCircle(): JSX.Element { const progress = useSharedValue(0) - const animatedProps = useAnimatedProps( - () => ({ - strokeDashoffset: CIRCLE_LENGTH * (1 - progress.value), - }), - [progress] - ) + const animatedProps = useAnimatedProps(() => ({ + strokeDashoffset: CIRCLE_LENGTH * (1 - progress.value), + })) useEffect(() => { if (isHoldToSwapPressed) { diff --git a/packages/wallet/src/features/transactions/swap/MaxAmountButton.tsx b/apps/mobile/src/features/transactions/swapRewrite/MaxAmountButton.tsx similarity index 87% rename from packages/wallet/src/features/transactions/swap/MaxAmountButton.tsx rename to apps/mobile/src/features/transactions/swapRewrite/MaxAmountButton.tsx index 5fe01af602e..8ea99bdbb7c 100644 --- a/packages/wallet/src/features/transactions/swap/MaxAmountButton.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/MaxAmountButton.tsx @@ -1,11 +1,12 @@ +import React from 'react' import { useTranslation } from 'react-i18next' import { StyleProp, ViewStyle } from 'react-native' +import Trace from 'src/components/Trace/Trace' +import { ElementName } from 'src/features/telemetry/constants' +import { useSwapFormContext } from 'src/features/transactions/swapRewrite/contexts/SwapFormContext' +import { maxAmountSpend } from 'src/utils/balance' import { Text, TouchableArea } from 'ui/src' -import { Trace } from 'utilities/src/telemetry/trace/Trace' -import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { ElementName } from 'wallet/src/telemetry/constants' -import { maxAmountSpend } from 'wallet/src/utils/balance' interface MaxAmountButtonProps { onSetMax: (amount: string, currencyField: CurrencyField) => void diff --git a/packages/wallet/src/features/transactions/swap/SwapArrowButton.tsx b/apps/mobile/src/features/transactions/swapRewrite/SwapArrowButton.tsx similarity index 92% rename from packages/wallet/src/features/transactions/swap/SwapArrowButton.tsx rename to apps/mobile/src/features/transactions/swapRewrite/SwapArrowButton.tsx index 83bdaa1a490..1a3f3974396 100644 --- a/packages/wallet/src/features/transactions/swap/SwapArrowButton.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/SwapArrowButton.tsx @@ -1,7 +1,7 @@ -import { useMemo } from 'react' +import React, { useMemo } from 'react' +import { Arrow } from 'src/components/icons/Arrow' import { Flex, TouchableArea, TouchableAreaProps, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { Arrow } from 'wallet/src/components/icons/Arrow' type SwapArrowButtonProps = Pick< TouchableAreaProps, diff --git a/packages/wallet/src/features/transactions/swap/SwapFlow.tsx b/apps/mobile/src/features/transactions/swapRewrite/SwapFlow.tsx similarity index 54% rename from packages/wallet/src/features/transactions/swap/SwapFlow.tsx rename to apps/mobile/src/features/transactions/swapRewrite/SwapFlow.tsx index 771ad911bfa..a95318685d7 100644 --- a/packages/wallet/src/features/transactions/swap/SwapFlow.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/SwapFlow.tsx @@ -1,37 +1,29 @@ -import { Dispatch, ReactNode, SetStateAction, useEffect, useState } from 'react' -import { Trace } from 'utilities/src/telemetry/trace/Trace' -import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' -import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import React, { Dispatch, ReactNode, SetStateAction, useEffect, useMemo, useState } from 'react' +import { useAppSelector } from 'src/app/hooks' +import { selectModalState } from 'src/features/modals/selectModalState' +import { ModalName, SectionName } from 'src/features/telemetry/constants' import { SwapFormContextProvider, SwapFormState, -} from 'wallet/src/features/transactions/contexts/SwapFormContext' +} from 'src/features/transactions/swapRewrite/contexts/SwapFormContext' import { SwapScreen, SwapScreenContextProvider, useSwapScreenContext, -} from 'wallet/src/features/transactions/contexts/SwapScreenContext' -import { - SwapTxContextProviderLegacyApi, - SwapTxContextProviderTradingApi, -} from 'wallet/src/features/transactions/contexts/SwapTxContext' -import { SwapFormButton } from 'wallet/src/features/transactions/swap/SwapFormButton' -import { SwapFormScreen } from 'wallet/src/features/transactions/swap/SwapFormScreen' -import { SwapReviewScreen } from 'wallet/src/features/transactions/swap/SwapReviewScreen' +} from 'src/features/transactions/swapRewrite/contexts/SwapScreenContext' +import { SwapTxContextProvider } from 'src/features/transactions/swapRewrite/contexts/SwapTxContext' +import { useOnCloseSwapModal } from 'src/features/transactions/swapRewrite/hooks/useOnCloseSwapModal' +import { SwapFormButton } from 'src/features/transactions/swapRewrite/SwapFormButton' +import { SwapFormScreen } from 'src/features/transactions/swapRewrite/SwapFormScreen' +import { SwapReviewScreen } from 'src/features/transactions/swapRewrite/SwapReviewScreen' import { TransactionModal, TransactionModalFooterContainer, -} from 'wallet/src/features/transactions/swap/TransactionModal' -import { TransactionModalProps } from 'wallet/src/features/transactions/swap/TransactionModalProps' -import { ModalName, SectionName } from 'wallet/src/telemetry/constants' +} from 'src/features/transactions/swapRewrite/TransactionModal' +import { getFocusOnCurrencyFieldFromInitialState } from 'src/features/transactions/swapRewrite/utils' +import { Trace } from 'utilities/src/telemetry/trace/Trace' -export function SwapFlow({ - prefilledState, - ...transactionModalProps -}: { - prefilledState?: SwapFormState - onClose: () => void -} & Omit): JSX.Element { +export function SwapFlow(): JSX.Element { // We need this additional `screen` state outside of the `SwapScreenContext` because the `SwapScreenContextProvider` needs to be inside the `BottomSheetModal`'s `Container`. const [screen, setScreen] = useState(SwapScreen.SwapForm) @@ -40,9 +32,11 @@ export function SwapFlow({ const showStickyReviewButton = screen === SwapScreen.SwapForm || screen === SwapScreen.SwapReviewHoldingToSwap + const onCloseSwapModal = useOnCloseSwapModal() + return ( - - + + {/* We render the `Review` button here instead of doing it inside `SwapFormScreen` so that it stays in place when the user is "holding to swap". @@ -111,23 +105,34 @@ function SwapReviewScreenDelayedRender(): JSX.Element { return } -function SwapContextsContainer({ - prefilledState, - children, -}: { - prefilledState: SwapFormState | undefined - children?: ReactNode -}): JSX.Element { - // conditionally render a different provider based on the active api gate. Each uses different hooks for data fetching. - const isTradingApiEnabled = useFeatureFlag(FEATURE_FLAGS.TradingApi) - const SwapTxContextProviderWrapper = isTradingApiEnabled - ? SwapTxContextProviderTradingApi - : SwapTxContextProviderLegacyApi +function SwapContextsContainer({ children }: { children?: ReactNode }): JSX.Element { + const onCloseSwapModal = useOnCloseSwapModal() + const { initialState } = useAppSelector(selectModalState(ModalName.Swap)) + + const prefilledState = useMemo( + (): SwapFormState | undefined => + initialState + ? { + customSlippageTolerance: initialState.customSlippageTolerance, + exactAmountFiat: initialState.exactAmountFiat, + exactAmountToken: initialState.exactAmountToken, + exactCurrencyField: initialState.exactCurrencyField, + focusOnCurrencyField: getFocusOnCurrencyFieldFromInitialState(initialState), + input: initialState.input ?? undefined, + output: initialState.output ?? undefined, + selectingCurrencyField: initialState.selectingCurrencyField, + txId: initialState.txId, + isFiatMode: false, + isSubmitting: false, + } + : undefined, + [initialState] + ) return ( - - {children} + + {children} ) diff --git a/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx b/apps/mobile/src/features/transactions/swapRewrite/SwapFormButton.tsx similarity index 77% rename from packages/wallet/src/features/transactions/swap/SwapFormButton.tsx rename to apps/mobile/src/features/transactions/swapRewrite/SwapFormButton.tsx index e370fde216a..33be3097189 100644 --- a/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx +++ b/apps/mobile/src/features/transactions/swapRewrite/SwapFormButton.tsx @@ -1,31 +1,30 @@ -import { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { TFunction, useTranslation } from 'react-i18next' -import { isWeb } from 'tamagui' -import { Button, Flex, Icons, Text } from 'ui/src' -import { Trace } from 'utilities/src/telemetry/trace/Trace' +import { useAppSelector as useMobileAppSelector } from 'src/app/hooks' +import Trace from 'src/components/Trace/Trace' import { selectHasSubmittedHoldToSwap, selectHasViewedReviewScreen, -} from 'wallet/src/features/behaviorHistory/selectors' -import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' +} from 'src/features/behaviorHistory/selectors' +import { ElementName } from 'src/features/telemetry/constants' +import { isWrapAction } from 'src/features/transactions/swap/utils' +import { useSwapFormContext } from 'src/features/transactions/swapRewrite/contexts/SwapFormContext' import { SwapScreen, useSwapScreenContext, -} from 'wallet/src/features/transactions/contexts/SwapScreenContext' -import { useTransactionModalContext } from 'wallet/src/features/transactions/contexts/TransactionModalContext' -import { useParsedSwapWarnings } from 'wallet/src/features/transactions/hooks/useParsedSwapWarnings' +} from 'src/features/transactions/swapRewrite/contexts/SwapScreenContext' import { HoldToSwapProgressCircle, PROGRESS_CIRCLE_SIZE, -} from 'wallet/src/features/transactions/swap/HoldToSwapProgressCircle' -import { isWrapAction } from 'wallet/src/features/transactions/swap/utils' +} from 'src/features/transactions/swapRewrite/HoldToSwapProgressCircle' +import { useParsedSwapWarnings } from 'src/features/transactions/swapRewrite/hooks/useParsedSwapWarnings' +import { useWalletRestore } from 'src/features/wallet/hooks' +import { Button, Flex, Icons, Text } from 'ui/src' import { WrapType } from 'wallet/src/features/transactions/types' import { createTransactionId } from 'wallet/src/features/transactions/utils' import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppSelector } from 'wallet/src/state' -import { ElementName } from 'wallet/src/telemetry/constants' export const HOLD_TO_SWAP_TIMEOUT = 3000 @@ -33,13 +32,13 @@ export function SwapFormButton(): JSX.Element { const { t } = useTranslation() const activeAccount = useActiveAccountWithThrow() - const { walletNeedsRestore } = useTransactionModalContext() const { screen, setScreen } = useSwapScreenContext() const { derivedSwapInfo, isSubmitting, updateSwapForm } = useSwapFormContext() const { blockingWarning } = useParsedSwapWarnings() const { wrapType, trade } = derivedSwapInfo + const { walletNeedsRestore } = useWalletRestore() const { isBlocked, isBlockedLoading } = useIsBlockedActiveAddress() const noValidSwap = !isWrapAction(wrapType) && !trade.trade @@ -56,15 +55,14 @@ export function SwapFormButton(): JSX.Element { const isHoldToSwapPressed = screen === SwapScreen.SwapReviewHoldingToSwap || isSubmitting - const hasViewedReviewScreen = useAppSelector(selectHasViewedReviewScreen) - const hasSubmittedHoldToSwap = useAppSelector(selectHasSubmittedHoldToSwap) + const hasViewedReviewScreen = useMobileAppSelector(selectHasViewedReviewScreen) + const hasSubmittedHoldToSwap = useMobileAppSelector(selectHasSubmittedHoldToSwap) const showHoldToSwapTip = hasViewedReviewScreen && !hasSubmittedHoldToSwap && activeAccount.type !== AccountType.Readonly // Force users to view regular review screen before enabling hold to swap // Disable for view only because onSwap action will fail - const enableHoldToSwap = - !isWeb && hasViewedReviewScreen && activeAccount.type !== AccountType.Readonly + const enableHoldToSwap = hasViewedReviewScreen && activeAccount.type !== AccountType.Readonly const onReview = useCallback( (nextScreen: SwapScreen) => { @@ -94,7 +92,7 @@ export function SwapFormButton(): JSX.Element { return ( - {!isWeb && !isHoldToSwapPressed && showHoldToSwapTip && } + {!isHoldToSwapPressed && showHoldToSwapTip && } - - - {unitag} - - - - - - {!!claimError && ( - - {claimError} - - )} - {showModal && ( )} - + + ) +} + +function TitleRow(): JSX.Element { + const { t } = useTranslation() + + return ( + + + {t('Choose a profile photo')} + + + {t('Upload your own or stick with your unique Unicon. You can always change this later.')} + + ) } diff --git a/apps/mobile/src/features/unitags/ChooseUnitag.tsx b/apps/mobile/src/features/unitags/ChooseUnitag.tsx new file mode 100644 index 00000000000..b7ecc8b5756 --- /dev/null +++ b/apps/mobile/src/features/unitags/ChooseUnitag.tsx @@ -0,0 +1,176 @@ +import { ADDRESS_ZERO } from '@uniswap/v3-sdk' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Keyboard } from 'react-native' +import { navigate } from 'src/app/navigation/rootNavigation' +import { Pill } from 'src/components/text/Pill' +import { TooltipInfoButton } from 'src/components/tooltip/TooltipButton' +import { ScreenRow } from 'src/components/unitags/ScreenRow' +import { UNITAG_SUFFIX } from 'src/features/unitags/constants' +import { UnitagInput } from 'src/features/unitags/UnitagInput' +import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' +import { useKeyboardLayout } from 'src/utils/useKeyboardLayout' +import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src' +import { fonts, iconSizes } from 'ui/src/theme' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { useUnitagError } from 'wallet/src/features/unitags/hooks' +import { useActiveAccountAddress, usePendingAccounts } from 'wallet/src/features/wallet/hooks' +import { shortenAddress } from 'wallet/src/utils/addresses' + +const LIVE_CHECK_DELAY_MS = 1000 + +export function ChooseUnitag({ + entryPoint, +}: { + entryPoint: OnboardingScreens.Landing | Screens.Home +}): JSX.Element { + const colors = useSporeColors() + const keyboard = useKeyboardLayout() + const compact = keyboard.isVisible && keyboard.containerHeight !== 0 + const minHeight = compact ? keyboard.containerHeight : 0 + const { t } = useTranslation() + const activeAddress = useActiveAccountAddress() + const pendingAccountAddress = Object.values(usePendingAccounts())?.[0]?.address + const unitagAddress = activeAddress || pendingAccountAddress + const [unitag, setUnitag] = useState(undefined) + const [showLiveCheck, setShowLiveCheck] = useState(false) + const { unitagError, loading } = useUnitagError(unitagAddress, unitag) + const isUnitagValid = !unitagError && !loading && !!unitag + const showValidUnitagLogo = isUnitagValid && showLiveCheck + + const onChange = (text: string | undefined): void => { + if (unitag !== text?.trim()) { + setShowLiveCheck(false) + } + setUnitag(text?.trim()) + } + + const onSubmit = (): void => { + Keyboard.dismiss() + } + + const onPressContinue = (): void => { + if (unitag) { + navigate(Screens.UnitagStack, { + screen: UnitagScreens.ChooseProfilePicture, + params: { entryPoint, unitag }, + }) + } + } + + const onPressMaybeLater = (): void => { + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.EditName, + params: { + importType: ImportType.CreateNew, + entryPoint: OnboardingEntryPoint.FreshInstallOrReplace, + }, + }) + } + + useEffect(() => { + const delayFn = setTimeout(() => { + setShowLiveCheck(true) + }, LIVE_CHECK_DELAY_MS) + + return () => { + clearTimeout(delayFn) + } + }, [unitag]) + + return ( + + + + + + + } + modalText={t( + `This username is a simple, user-friendly way to use your address in transactions. Your current address remains unchanged and secure.` + )} + modalTitle={t('An easier way to receive')} + size={iconSizes.icon24} + /> + } + /> + + + + + {entryPoint === OnboardingScreens.Landing && ( + + )} + + + + + ) +} + +function TitleRow(): JSX.Element { + const { t } = useTranslation() + + return ( + + + {t('Claim your username')} + + + {t( + 'This is your unique name that people can send funds to and use to find you across defi.' + )} + + + ) +} diff --git a/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx index 2112f3c608b..2a67bff2743 100644 --- a/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx +++ b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx @@ -1,418 +1,38 @@ -import { NativeStackScreenProps } from '@react-navigation/native-stack' -import { ADDRESS_ZERO } from '@uniswap/v3-sdk' -import { default as React, useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { ActivityIndicator, Keyboard } from 'react-native' -import { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated' -import { navigate } from 'src/app/navigation/rootNavigation' -import { UnitagStackParamList } from 'src/app/navigation/types' -import Trace from 'src/components/Trace/Trace' -import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' -import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' -import { useAddBackButton } from 'src/utils/useAddBackButton' -import { - AnimatePresence, - AnimatedFlex, - Button, - Flex, - Icons, - Text, - TouchableArea, - useSporeColors, -} from 'ui/src' -import Unitag from 'ui/src/assets/graphics/unitag.svg' -import InfoCircle from 'ui/src/assets/icons/info-circle.svg' -import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' -import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { TextInput } from 'wallet/src/components/input/TextInput' -import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { Pill } from 'wallet/src/components/text/Pill' -import { OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' -import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' -import { useCanClaimUnitagName } from 'wallet/src/features/unitags/hooks' -import { useActiveAccountAddress, usePendingAccounts } from 'wallet/src/features/wallet/hooks' -import { ElementName, ModalName } from 'wallet/src/telemetry/constants' -import { shortenAddress } from 'wallet/src/utils/addresses' -import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' - -const MAX_UNITAG_CHAR_LENGTH = 20 - -const MAX_INPUT_FONT_SIZE = 36 -const MIN_INPUT_FONT_SIZE = 22 -const MAX_CHAR_PIXEL_WIDTH = 20 - -// Used in dynamic font size width calculation to ignore `.` characters -const UNITAG_SUFFIX_CHARS_ONLY = UNITAG_SUFFIX.replaceAll('.', '') -const TEXT_INPUT_PLACEHOLDER = 'yourname' - -// Accounts for height of image, gap between image and name, and spacing from top of titles -const UNITAG_NAME_ANIMATE_DISTANCE_Y = imageSizes.image100 + spacing.spacing36 + spacing.spacing48 - -type Props = NativeStackScreenProps - -export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { - const { entryPoint, importType } = route.params - - useAddBackButton(navigation) - const { t } = useTranslation() - const colors = useSporeColors() - - const activeAddress = useActiveAccountAddress() - const pendingAccountAddress = Object.values(usePendingAccounts())?.[0]?.address - const unitagAddress = activeAddress || pendingAccountAddress - - const [showInfoModal, setShowInfoModal] = useState(false) - const [showTextInputView, setShowTextInputView] = useState(true) - const [unitagInputValue, setUnitagInputValue] = useState(undefined) - const [isCheckingUnitag, setIsCheckingUnitag] = useState(false) - const [unitagToCheck, setUnitagToCheck] = useState(undefined) - - const addressViewOpacity = useSharedValue(1) - const unitagInputContainerTranslateY = useSharedValue(0) - const addressViewAnimatedStyle = useAnimatedStyle(() => { - return { - opacity: addressViewOpacity.value, - } - }) - - const { error: canClaimUnitagNameError, loading: loadingUnitagErrorCheck } = - useCanClaimUnitagName(unitagAddress, unitagToCheck) - - const { onLayout, fontSize, onSetFontSize } = useDynamicFontSizing( - MAX_CHAR_PIXEL_WIDTH, - MAX_INPUT_FONT_SIZE, - MIN_INPUT_FONT_SIZE - ) - - useEffect(() => { - const unsubscribe = navigation.addListener('focus', () => { - // Reset the Unitag to check - setUnitagToCheck(undefined) - - // When returning back to this screen, handle animating the Unitag logo out and text input in - if (showTextInputView) { - return - } - - unitagInputContainerTranslateY.value = withTiming( - unitagInputContainerTranslateY.value - UNITAG_NAME_ANIMATE_DISTANCE_Y, - { - duration: ONE_SECOND_MS / 2, - } - ) - setTimeout(() => { - setShowTextInputView(true) - addressViewOpacity.value = withTiming(1, { duration: 500 }) - }, ONE_SECOND_MS) - }) - - return unsubscribe - }, [ - navigation, - showTextInputView, - setShowTextInputView, - addressViewOpacity, - unitagInputContainerTranslateY, - ]) - - const onChangeTextInput = useCallback( - (text: string): void => { - if (text.length > MAX_UNITAG_CHAR_LENGTH) { - return - } - - if (text.length === 0) { - onSetFontSize(TEXT_INPUT_PLACEHOLDER + UNITAG_SUFFIX_CHARS_ONLY) - } else { - onSetFontSize(text + UNITAG_SUFFIX_CHARS_ONLY) - } - - setUnitagInputValue(text?.trim()) - }, - [onSetFontSize, setUnitagInputValue] - ) - - const onPressAddressTooltip = (): void => { - Keyboard.dismiss() - setShowInfoModal(true) - } - - const onPressMaybeLater = (): void => { - navigate(Screens.OnboardingStack, { - screen: OnboardingScreens.EditName, - params: { - importType, - entryPoint: - entryPoint === OnboardingScreens.Landing - ? OnboardingEntryPoint.FreshInstallOrReplace - : OnboardingEntryPoint.Sidebar, - }, - }) - } - - const navigateWithAnimation = useCallback( - (unitag: string) => { - // Animate the Unitag logo in and text input out - setShowTextInputView(false) - addressViewOpacity.value = withTiming(0, { duration: 500 }) - // Intentionally delay 1s to allow enter/exit animations to finish - unitagInputContainerTranslateY.value = withDelay( - ONE_SECOND_MS, - withTiming(unitagInputContainerTranslateY.value + UNITAG_NAME_ANIMATE_DISTANCE_Y, { - duration: ONE_SECOND_MS / 2, - }) - ) - // Navigate to ChooseProfilePicture screen after 1s delay to allow animations to finish - setTimeout(() => { - navigate( - entryPoint === OnboardingScreens.Landing || entryPoint === OnboardingEntryPoint.Sidebar - ? Screens.OnboardingStack - : Screens.UnitagStack, - { - screen: UnitagScreens.ChooseProfilePicture, - params: { entryPoint, unitag, importType }, - } - ) - }, ONE_SECOND_MS) - }, - [addressViewOpacity, entryPoint, importType, unitagInputContainerTranslateY] - ) - - // Handle when useUnitagError completes loading and returns a result after onPressContinue is called - useEffect(() => { - if (isCheckingUnitag && !!unitagToCheck && !loadingUnitagErrorCheck) { - setIsCheckingUnitag(false) - // If unitagError is defined, it's rendered in UI - if (!canClaimUnitagNameError) { - navigateWithAnimation(unitagToCheck) - } - } - }, [ - canClaimUnitagNameError, - loadingUnitagErrorCheck, - unitagToCheck, - isCheckingUnitag, - navigateWithAnimation, - ]) - - const onPressContinue = (): void => { - if (unitagInputValue !== unitagToCheck) { - setIsCheckingUnitag(true) - setUnitagToCheck(unitagInputValue) - } - } +import { useHeaderHeight } from '@react-navigation/elements' +import React from 'react' +import { KeyboardAvoidingView, StyleSheet } from 'react-native' +import { UnitagStackScreenProp } from 'src/app/navigation/types' +import { Screen, SHORT_SCREEN_HEADER_HEIGHT_RATIO } from 'src/components/layout/Screen' +import { ChooseUnitag } from 'src/features/unitags/ChooseUnitag' +import { UnitagScreens } from 'src/screens/Screens' +import { useDeviceInsets } from 'ui/src' +import { isIOS } from 'wallet/src/utils/platform' + +export function ClaimUnitagScreen({ + route, +}: UnitagStackScreenProp): JSX.Element { + const { entryPoint } = route.params + const headerHeight = useHeaderHeight() + const insets = useDeviceInsets() return ( - - { - onLayout(event) - onSetFontSize(TEXT_INPUT_PLACEHOLDER + UNITAG_SUFFIX_CHARS_ONLY) - }}> - {/* Fixed text that animates in when TextInput is animated out */} - - {!showTextInputView && ( - - - {unitagInputValue} - - - - - - )} - - {showTextInputView && ( - - - - {UNITAG_SUFFIX} - - - )} - - - - - {shortenAddress(unitagAddress ?? ADDRESS_ZERO)} - - { - Keyboard.dismiss() - setShowInfoModal(true) - }}> - - - - {canClaimUnitagNameError && unitagToCheck === unitagInputValue && ( - - - {canClaimUnitagNameError} - - - )} - - - {(entryPoint === OnboardingScreens.Landing || - entryPoint === OnboardingEntryPoint.Sidebar) && ( - - - - {t('Maybe later')} - - - - )} - - - {showInfoModal && ( - setShowInfoModal(false)} - /> - )} - + + + + + ) } -const InfoModal = ({ - unitag, - unitagAddress, - onClose, -}: { - unitag: string | undefined - unitagAddress: string | undefined - onClose: () => void -}): JSX.Element => { - const colors = useSporeColors() - const { t } = useTranslation() - - return ( - - - - - - - - {unitag ? unitag : TEXT_INPUT_PLACEHOLDER} - - {UNITAG_SUFFIX} - - - - - } - modalName={ModalName.TooltipContent} - title={t('A simplified address')} - onClose={onClose} - /> - ) -} +const styles = StyleSheet.create({ + base: { + flex: 1, + justifyContent: 'flex-end', + }, +}) diff --git a/apps/mobile/src/features/unitags/ConfirmationElements.tsx b/apps/mobile/src/features/unitags/ConfirmationElements.tsx deleted file mode 100644 index c1a31913f90..00000000000 --- a/apps/mobile/src/features/unitags/ConfirmationElements.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { Flex, Image, Text, useSporeColors } from 'ui/src' -import { DAI_LOGO, ENS_LOGO, ETH_LOGO, FROGGY, OPENSEA_LOGO, USDC_LOGO } from 'ui/src/assets' -import HeartIcon from 'ui/src/assets/icons/heart.svg' -import SendIcon from 'ui/src/assets/icons/send-action.svg' -import { colors, iconSizes, imageSizes, opacify } from 'ui/src/theme' -import { Arrow } from 'wallet/src/components/icons/Arrow' - -export const FroggyElement = (): JSX.Element => { - return ( - - - - ) -} - -export const OpenseaElement = (): JSX.Element => { - return ( - - - - ) -} - -export const SwapElement = (): JSX.Element => { - const sporeColors = useSporeColors() - return ( - - - - - ETH - - - - - - - DAI - - - - ) -} - -export const ENSElement = (): JSX.Element => { - return ( - - - - ) -} - -export const ReceiveUSDCElement = (): JSX.Element => { - return ( - - - +100 - - - - ) -} - -export const SendElement = (): JSX.Element => { - return ( - - - - ) -} - -export const HeartElement = (): JSX.Element => { - return ( - - - - ) -} - -export const TextElement = ({ text }: { text: string }): JSX.Element => { - return ( - - - {text} - - - ) -} - -export const EmojiElement = ({ emoji }: { emoji: string }): JSX.Element => { - return ( - - - {emoji} - - - ) -} diff --git a/apps/mobile/src/features/unitags/EditProfileScreen.tsx b/apps/mobile/src/features/unitags/EditProfileScreen.tsx new file mode 100644 index 00000000000..a6e8e3a9252 --- /dev/null +++ b/apps/mobile/src/features/unitags/EditProfileScreen.tsx @@ -0,0 +1,229 @@ +import { isEqual } from 'lodash' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Keyboard } from 'react-native' +import { UnitagStackScreenProp } from 'src/app/navigation/types' +import { TextInput } from 'src/components/input/TextInput' +import { Screen } from 'src/components/layout/Screen' +import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal' +import { ScreenRow } from 'src/components/unitags/ScreenRow' +import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' +import { UNITAG_SUFFIX } from 'src/features/unitags/constants' +import { UnitagScreens } from 'src/screens/Screens' +import { Button, Flex, Icons, Text, useDeviceInsets } from 'ui/src' +import { iconSizes, imageSizes } from 'ui/src/theme' +import { ChainId } from 'wallet/src/constants/chains' +import { useENS } from 'wallet/src/features/ens/useENS' +import { useUnitagUpdateMetadataMutation } from 'wallet/src/features/unitags/api' +import { useUnitag } from 'wallet/src/features/unitags/hooks' +import { ProfileMetadata } from 'wallet/src/features/unitags/types' +import { shortenAddress } from 'wallet/src/utils/addresses' + +const isProfileMetadataEdited = ( + loading: boolean, + updatedMetadata: ProfileMetadata, + initialMetadata?: ProfileMetadata +): boolean => { + return !!initialMetadata && !loading && isEqual(updatedMetadata, initialMetadata) +} + +export function EditProfileScreen({ + route, +}: UnitagStackScreenProp): JSX.Element { + const { address } = route.params + const unitag = useUnitag(address) + const { name: ensName } = useENS(ChainId.Mainnet, address) + const insets = useDeviceInsets() + const { t } = useTranslation() + const [showModal, setShowModal] = useState(false) + const [imageUri, setImageUri] = useState() + const [bioInput, setBioInput] = useState() + const [urlInput, setUrlInput] = useState() + const [twitterInput, setTwitterInput] = useState() + const updatedMetadata: ProfileMetadata = { + avatar: imageUri, + description: bioInput, + url: urlInput, + twitter: twitterInput, + } + const [ + updateUnitagMetadata, + { called: updateRequestMade, loading: updateResponseLoading, data: updateResponse }, + ] = useUnitagUpdateMetadataMutation(unitag?.username ?? address) // Save button can't be pressed until unitag is loaded anyways so this is fine + const profileMetadataEdited = isProfileMetadataEdited( + updateResponseLoading, + updatedMetadata, + updateResponse?.metadata ?? unitag?.metadata + ) + + useEffect(() => { + // Only want to set values on first time unitag loads, when we have not yet made the PUT request + if (!updateRequestMade && unitag?.metadata) { + setImageUri(unitag.metadata.avatar) + setBioInput(unitag.metadata.description) + setUrlInput(unitag.metadata.url) + setTwitterInput(unitag.metadata.twitter) + } + }, [updateRequestMade, unitag?.metadata]) + + const openModal = (): void => { + setShowModal(true) + } + + const onCloseModal = (): void => { + setShowModal(false) + } + + const onPressSaveChanges = async (): Promise => { + await updateUnitagMetadata({ + address, + metadata: updatedMetadata, + }) + } + + return ( + + + } // Need this to center Edit profile text + /> + + + + + + + + + + + + + + + + + {unitag?.username} + {UNITAG_SUFFIX} + + + {shortenAddress(address)} + + + + + + {t('Bio')} + + + + + + {t('Website')} + + + + + + {t('Twitter')} + + + + {ensName && ( + + + {t('ENS')} + + + {ensName} + + + )} + + + + + + {showModal && ( + + )} + + ) +} diff --git a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx deleted file mode 100644 index 041a32ed2b5..00000000000 --- a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import { isEqual } from 'lodash' -import React, { useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { Keyboard, KeyboardAvoidingView, StyleSheet } from 'react-native' -import ContextMenu from 'react-native-context-menu-view' -import { UnitagStackScreenProp } from 'src/app/navigation/types' -import { BackHeader } from 'src/components/layout/BackHeader' -import { Screen } from 'src/components/layout/Screen' -import { ChangeUnitagModal } from 'src/components/unitags/ChangeUnitagModal' -import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal' -import { DeleteUnitagModal } from 'src/components/unitags/DeleteUnitagModal' -import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' -import { HeaderRadial } from 'src/features/externalProfile/ProfileHeader' -import { tryUploadAvatar } from 'src/features/unitags/avatars' -import { Screens, UnitagScreens } from 'src/screens/Screens' -import { - Button, - Flex, - Icons, - LinearGradient, - ScrollView, - Text, - useSporeColors, - useUniconColors, -} from 'ui/src' -import { borderRadii, fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' -import { TextInput } from 'wallet/src/components/input/TextInput' -import { ChainId } from 'wallet/src/constants/chains' -import { useENS } from 'wallet/src/features/ens/useENS' -import { pushNotification } from 'wallet/src/features/notifications/slice' -import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { - useUnitagGetAvatarUploadUrlQuery, - useUnitagUpdateMetadataMutation, -} from 'wallet/src/features/unitags/api' -import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' -import { useUnitagUpdater } from 'wallet/src/features/unitags/context' -import { useUnitagByAddress } from 'wallet/src/features/unitags/hooks' -import { ProfileMetadata } from 'wallet/src/features/unitags/types' -import { useAppDispatch } from 'wallet/src/state' -import { shortenAddress } from 'wallet/src/utils/addresses' -import { useExtractedColors } from 'wallet/src/utils/colors' -import { isIOS } from 'wallet/src/utils/platform' - -const BIO_TEXT_INPUT_LINES = 6 - -const isProfileMetadataEdited = ( - loading: boolean, - updatedMetadata: ProfileMetadata, - initialMetadata?: ProfileMetadata -): boolean => { - return !loading && !isEqual(updatedMetadata, initialMetadata) -} - -export function EditUnitagProfileScreen({ - route, -}: UnitagStackScreenProp): JSX.Element { - const { address, unitag, entryPoint } = route.params - const { t } = useTranslation() - const colors = useSporeColors() - const dispatch = useAppDispatch() - - const { name: ensName } = useENS(ChainId.Mainnet, address) - const { refetchUnitagsCounter } = useUnitagUpdater() - const { - unitag: retrievedUnitag, - loading, - refetch: refetchUnitagByAddress, - } = useUnitagByAddress(address) - const unitagMetadata = retrievedUnitag?.metadata - - const [showAvatarModal, setShowAvatarModal] = useState(false) - const [avatarImageUri, setAvatarImageUri] = useState() - const [bioInput, setBioInput] = useState() - const [twitterInput, setTwitterInput] = useState() - const [showDeleteUnitagModal, setShowDeleteUnitagModal] = useState(false) - const [showChangeUnitagModal, setShowChangeUnitagModal] = useState(false) - - const updatedMetadata: ProfileMetadata = { - avatar: avatarImageUri, - description: bioInput, - twitter: twitterInput, - } - - const [ - updateUnitagMetadata, - { called: updateRequestMade, loading: updateResponseLoading, data: updateResponse }, - ] = useUnitagUpdateMetadataMutation(unitag) - - const { loading: avatarUploadUrlLoading, data: avatarUploadUrlResponse } = - useUnitagGetAvatarUploadUrlQuery({ username: retrievedUnitag?.username }) - - const profileMetadataEdited = isProfileMetadataEdited( - updateResponseLoading, - updatedMetadata, - updateResponse?.metadata ?? unitagMetadata - ) - - // Force refetch of canClaimUnitag if refetchUnitagsCounter changes - useEffect(() => { - refetchUnitagByAddress?.() - }, [refetchUnitagsCounter, refetchUnitagByAddress]) - - useEffect(() => { - // Only want to set values on first time unitag loads, when we have not yet made the PUT request - if (!updateRequestMade && unitagMetadata) { - setAvatarImageUri(unitagMetadata.avatar) - setBioInput(unitagMetadata.description) - setTwitterInput(unitagMetadata.twitter) - } - }, [updateRequestMade, unitagMetadata]) - - const { colors: avatarColors } = useExtractedColors(avatarImageUri) - const { gradientStart: uniconGradientStart, gradientEnd: uniconGradientEnd } = - useUniconColors(address) - - // Wait for avatar, then render avatar extracted colors or unicon colors if no avatar - const fixedGradientColors = useMemo(() => { - if (avatarImageUri || (avatarImageUri && !avatarColors)) { - return [colors.surface1.val, colors.surface1.val] - } - if (avatarImageUri && avatarColors && avatarColors.base) { - return [avatarColors.base, avatarColors.base] - } - return [uniconGradientStart, uniconGradientEnd] - }, [avatarColors, avatarImageUri, uniconGradientEnd, uniconGradientStart, colors.surface1.val]) - - const openAvatarModal = (): void => { - setShowAvatarModal(true) - } - - const onCloseAvatarModal = (): void => { - setShowAvatarModal(false) - } - - const onPressSaveChanges = async (): Promise => { - Keyboard.dismiss() - - // Try to upload avatar or skip avatar upload if not needed - const { success, skipped } = await tryUploadAvatar({ - avatarImageUri, - avatarUploadUrlResponse, - avatarUploadUrlLoading, - }) - - // Display error if avatar upload failed - if (!success) { - displayErrorNotification() - return - } - - try { - const uploadedNewAvatar = success && !skipped - await updateProfileMetadata(uploadedNewAvatar) - } catch (e) { - displayErrorNotification() - } - } - - const updateProfileMetadata = async (uploadedNewAvatar: boolean): Promise => { - // If new avatar was uploaded, update metadata.avatar to be the S3 file location - const metadata = uploadedNewAvatar - ? { ...updatedMetadata, avatar: avatarUploadUrlResponse?.avatarUrl } - : updatedMetadata - - await updateUnitagMetadata({ address, metadata, clearAvatar: metadata.avatar === undefined }) - dispatch( - pushNotification({ - type: AppNotificationType.Success, - title: t('Profile updated'), - }) - ) - - if (uploadedNewAvatar) { - setAvatarImageUri(avatarUploadUrlResponse?.avatarUrl) - } - } - - const displayErrorNotification = (): void => { - dispatch( - pushNotification({ - type: AppNotificationType.Error, - errorMessage: t('Error updating profile. Please try again.'), - }) - ) - } - - const menuActions = useMemo(() => { - return [ - { title: t('Edit username'), systemIcon: 'pencil' }, - { title: t('Delete username'), systemIcon: 'trash', destructive: true }, - ] - }, [t]) - - return ( - - - {/* Necessary to handle different header configuration when navigating from SettingsStack vs. UnitagsStack */} - {entryPoint === Screens.SettingsWallet ? ( - { - // Emitted index based on order of menu action array - // Edit username - if (e.nativeEvent.index === 0) { - setShowChangeUnitagModal(true) - } - // Delete username - if (e.nativeEvent.index === 1) { - setShowDeleteUnitagModal(true) - } - }}> - - - - - } - p="$spacing16"> - {t('Edit profile')} - - ) : ( - - - {t('Edit profile')} - - - )} - - - - - - - - {avatarImageUri && avatarColors?.primary ? ( - - ) : null} - - - - - - - - - - - - - - {unitag} - {UNITAG_SUFFIX} - - - {shortenAddress(address)} - - - - - - - {t('Bio')} - - {!loading ? ( - - ) : null} - - - - {t('Twitter')} - - {!loading ? ( - - ) : null} - - {ensName && ( - - - {t('ENS')} - - - {ensName} - - - )} - - - - - - {showAvatarModal && ( - - )} - - {showDeleteUnitagModal && ( - setShowDeleteUnitagModal(false)} - /> - )} - {showChangeUnitagModal && ( - setShowChangeUnitagModal(false)} - /> - )} - - ) -} - -const styles = StyleSheet.create({ - base: { - flex: 1, - justifyContent: 'flex-end', - }, - expand: { - flexGrow: 1, - }, - headerGradient: { - borderRadius: borderRadii.rounded20, - flex: 1, - opacity: 0.2, - }, -}) diff --git a/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx index abebf5b99f1..9eb81b91620 100644 --- a/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx +++ b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx @@ -1,170 +1,90 @@ -import React, { useMemo } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' import { navigate } from 'src/app/navigation/rootNavigation' import { UnitagStackScreenProp } from 'src/app/navigation/types' -import { AnimateInOrder } from 'src/components/animation/AnimateInOrder' import { Screen } from 'src/components/layout/Screen' -import { UnitagWithProfilePicture } from 'src/components/unitags/UnitagWithProfilePicture' -import { - EmojiElement, - ENSElement, - FroggyElement, - HeartElement, - OpenseaElement, - ReceiveUSDCElement, - SendElement, - SwapElement, - TextElement, -} from 'src/features/unitags/ConfirmationElements' +import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' +import { UNITAG_SUFFIX } from 'src/features/unitags/constants' import { Screens, UnitagScreens } from 'src/screens/Screens' -import { AnimatePresence, Button, Flex, Text, useDeviceDimensions, useDeviceInsets } from 'ui/src' -import { spacing } from 'ui/src/theme' -import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' +import { Button, Flex, Text, useDeviceInsets } from 'ui/src' +import { imageSizes } from 'ui/src/theme' export function UnitagConfirmationScreen({ route, }: UnitagStackScreenProp): JSX.Element { const { unitag, address, profilePictureUri } = route.params - const dimensions = useDeviceDimensions() + const insets = useDeviceInsets() const { t } = useTranslation() - const boxWidth = dimensions.fullWidth - insets.left - insets.right - spacing.spacing32 - const onPressCustomize = (): void => { navigate(Screens.UnitagStack, { screen: UnitagScreens.EditProfile, params: { address, - unitag, - entryPoint: UnitagScreens.UnitagConfirmation, }, }) } - const onPressDone = (): void => { + const onPressHome = (): void => { navigate(Screens.Home) } - const elementsToAnimate = useMemo( - () => [ - { element: , coordinates: { x: 5, y: 0 } }, - { element: , coordinates: { x: 10, y: 2 } }, - { element: , coordinates: { x: 8.2, y: 4 } }, - { element: , coordinates: { x: 9, y: 7 } }, - { element: , coordinates: { x: 10, y: 10 } }, - { element: , coordinates: { x: 1, y: 8.5 } }, - { element: , coordinates: { x: 0, y: 5 } }, - { element: , coordinates: { x: 1, y: 2 } }, - { element: , coordinates: { x: 3.5, y: 2.5 } }, - ], - [t] - ) - return ( - - - - - + + + + + + + + - - - - - {elementsToAnimate.map(({ element, coordinates }, index) => ( - - {element} - - ))} - - - - - - - - {t('You got it!')} - - - {t( - '{{unitag}}{{unitagSuffix}} is ready to send and receive crypto. Continue to build out your wallet by customizing your profile', - { unitag, unitagSuffix: UNITAG_SUFFIX } - )} - - - - - + row + backgroundColor="$accent2" + borderRadius="$rounded32" + gap="$spacing8" + px="$spacing32" + py="$spacing4" + zIndex="$popover"> + + {unitag} + + + + + + + {t('You’ve got it!')} + + + {t( + '{{unitag}}{{unitagSuffix}} is ready to send and receive crypto. Continue to build out your wallet by customizing your profile', + { unitag, unitagSuffix: UNITAG_SUFFIX } + )} + + + + + + + ) } - -// Calculates top and left insets for absolute positioned element based -// on a 10x10 coordinate system where top left is 0,0. -const getInsetPropsForCoordinates = ( - boxWidth: number, - x: number, - y: number -): { top?: number; right?: number; bottom?: number; left?: number } => { - const unitSize = 10 - const unit = boxWidth / unitSize - - let top - let bottom - let left - let right - - if (x < unitSize / 2) { - left = x * unit - } else if (x > unitSize / 2) { - right = (unitSize - x) * unit - } - - if (y < unitSize / 2) { - top = y * unit - } else if (y > unitSize / 2) { - bottom = (unitSize - y) * unit - } - - return { top, right, bottom, left } -} diff --git a/apps/mobile/src/features/unitags/UnitagInput.tsx b/apps/mobile/src/features/unitags/UnitagInput.tsx new file mode 100644 index 00000000000..8426f9e1093 --- /dev/null +++ b/apps/mobile/src/features/unitags/UnitagInput.tsx @@ -0,0 +1,217 @@ +import React, { useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + LayoutChangeEvent, + LayoutRectangle, + StyleSheet, + TextInput as NativeTextInput, +} from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { AddressDisplay } from 'src/components/AddressDisplay' +import { SpinningLoader } from 'src/components/loading/SpinningLoader' +import { WalletSelectorModal } from 'src/components/unitags/WalletSelectorModal' +import InputWithSuffix from 'src/features/import/InputWithSuffix' +import { UNITAG_SUFFIX } from 'src/features/unitags/constants' +import { AnimatedFlex, Flex, Icons, Text } from 'ui/src' +import Unitag from 'ui/src/assets/icons/unitag.svg' +import { fonts, iconSizes } from 'ui/src/theme' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' +import { setAccountAsActive } from 'wallet/src/features/wallet/slice' +import { useAppDispatch } from 'wallet/src/state' + +const INPUT_MIN_HEIGHT = 120 +const INPUT_MIN_HEIGHT_SHORT = 90 + +type UnitagInputProps = { + activeAddress: Maybe
+ value: string | undefined + errorMessage: string | undefined + onChange: (text: string | undefined) => void + placeholderLabel: string | undefined + onSubmit?: () => void + inputSuffix?: string + liveCheck?: boolean + loading?: boolean + showUnitagLogo: boolean + onBlur?: () => void + onFocus?: () => void +} + +export function UnitagInput({ + activeAddress, + value, + inputSuffix, + onBlur, + onFocus, + onSubmit, + onChange, + liveCheck, + loading, + placeholderLabel, + showUnitagLogo, + errorMessage, +}: UnitagInputProps): JSX.Element { + const { t } = useTranslation() + const placeholderUnitag = t('yourname') + UNITAG_SUFFIX + + const [focused, setFocused] = useState(false) + const [layout, setLayout] = useState() + const [showModal, setShowModal] = useState(false) + const textInputRef = useRef(null) + const dispatch = useAppDispatch() + const activeAccount = useActiveAccount() + + const INPUT_FONT_SIZE = fonts.heading2.fontSize + const INPUT_MAX_FONT_SIZE_MULTIPLIER = fonts.heading2.maxFontSizeMultiplier + + const handleBlur = (): void => { + setFocused(false) + onBlur?.() + } + + const handleFocus = (): void => { + setFocused(true) + onFocus?.() + // Need this to allow for focus on click on container. + textInputRef?.current?.focus() + } + + const handleSubmit = (): void => { + onSubmit && onSubmit() + } + + const onAddressPress = (): void => setShowModal(true) + + const onCloseModal = (): void => setShowModal(false) + + const onPressAccountOption = (account: Account): void => { + dispatch(setAccountAsActive(account.address)) + setShowModal(false) + } + return ( + <> + + + + + + {loading && ( + + + + )} + {showUnitagLogo && ( + + + + )} + + {!value && ( + setLayout(event.nativeEvent.layout)}> + {placeholderLabel && ( + + {placeholderLabel} + + )} + + {UNITAG_SUFFIX} + + + )} + + + {errorMessage && value && (liveCheck || !focused) && ( + + + {errorMessage} + + + )} + + {activeAddress && ( + + + + + + + + )} + + + {showModal && ( + + )} + + ) +} + +const styles = StyleSheet.create({ + placeholderLabelStyle: { + flexShrink: 1, + }, +}) diff --git a/apps/mobile/src/features/unitags/avatars.ts b/apps/mobile/src/features/unitags/avatars.ts deleted file mode 100644 index a54ad0818d4..00000000000 --- a/apps/mobile/src/features/unitags/avatars.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { FetchResult } from '@apollo/client' -import axios from 'axios' -import { Platform } from 'react-native' -import { logger } from 'utilities/src/logger/logger' -import { getUnitagAvatarUploadUrl } from 'wallet/src/features/unitags/api' -import { - UnitagAvatarUploadCredentials, - UnitagGetAvatarUploadUrlResponse, - UnitagUpdateMetadataRequestBody, - UnitagUpdateMetadataResponse, -} from 'wallet/src/features/unitags/types' - -export function isLocalFileUri(imageUri: string): boolean { - const localFilePatterns = [ - 'file://', // iOS local file prefix - 'content://', // Android Content Provider - '/storage/', // Android internal storage (absolute path) - '/data/', // Android internal data storage (absolute path) - ] - - // Check if the imageUri starts with any of the local file patterns - return localFilePatterns.some((pattern) => imageUri.startsWith(pattern)) -} - -export async function uploadFileToS3( - imageUri: string, - creds: UnitagAvatarUploadCredentials -): Promise<{ success: boolean }> { - if (!creds.preSignedUrl || !creds.s3UploadFields) { - return { success: false } - } - - // Standardize the uri for iOS and Android - const uri = Platform.OS === 'android' ? imageUri : imageUri.replace('file://', '') - const formData = new FormData() - - // Add the S3 fields to the form data - Object.entries(creds.s3UploadFields).forEach(([key, value]) => { - formData.append(key, value) - }) - - // Get the file as a blob to set the Content-Type - const response = await fetch(uri) - const blob = await response.blob() - formData.append('Content-Type', blob.type) - - // Add the file to the form data. We ignore the function signature and input an object with keys uri, type, and name - // This is the argument that react-native's FormData expects, but for some reason our project thinks it's using typescript's FormData - // Ignoring the typecheck and forcing the object to be Blob works though - formData.append('file', { - uri, - type: blob.type, - name: uri, - } as unknown as Blob) - - // Send the post request to S3 using pre-signed URL and s3 fields - try { - await axios.post(creds.preSignedUrl, formData, { - headers: { - 'Content-Type': 'multipart/form-data', // Important for S3 to process the file correctly - }, - }) - logger.info('unitags/utils.ts', 'uploadFileToS3', 'Avatar uploaded to S3 successfully') - return { success: true } - } catch (error) { - logger.error(error, { - tags: { file: 'unitags/utils.ts', function: 'uploadFileToS3' }, - }) - return { success: false } - } -} - -/** - * Uploads an image to S3 and updates the avatar for a given username and address. - * Expects imageUri to be a local file, it uploads the file to S3 and updates the avatar URL in the metadata. - * - * @param {string} username - The newly claimed unitag. - * @param {Address} address - The address of the unitag. - * @param {string} imageUri - The URI of the new avatar image (either a local file or external url). - * @param {(variables: UnitagUpdateMetadataRequestBody) => Promise>} updateUnitagMetadata - The function to call to update the metadata on the backend. - * @returns {Promise} - A promise that resolves to a boolean indicating whether the avatar was successfully updated. - */ -export async function uploadAndUpdateAvatarAfterClaim( - username: string, - address: Address, - imageUri: string, - updateUnitagMetadata: (variables: UnitagUpdateMetadataRequestBody) => Promise< - FetchResult<{ - data: UnitagUpdateMetadataResponse - }> - > -): Promise<{ success: boolean }> { - try { - // First get pre-signedUrl and s3UploadFields from the backend - const { data: avatarUploadUrlResponse } = await getUnitagAvatarUploadUrl(username) - - // Then upload to S3 - const { success: uploadSuccess } = await uploadFileToS3(imageUri, { - preSignedUrl: avatarUploadUrlResponse.preSignedUrl, - s3UploadFields: avatarUploadUrlResponse.s3UploadFields, - }) - - // Check if upload succeeded - if (!uploadSuccess) { - return { success: false } - } - - // Then update profile metadata with the image url - await updateUnitagMetadata({ - address, - metadata: { - avatar: avatarUploadUrlResponse.avatarUrl, - }, - }) - return { success: true } - } catch (e) { - logger.error(e, { - tags: { file: 'unitags/utils.ts', function: 'uploadAndUpdateAvatarAfterClaim' }, - }) - return { success: false } - } -} - -export const tryUploadAvatar = async ({ - avatarImageUri, - avatarUploadUrlResponse, - avatarUploadUrlLoading, -}: { - avatarImageUri: string | undefined - avatarUploadUrlResponse: UnitagGetAvatarUploadUrlResponse | undefined - avatarUploadUrlLoading: boolean -}): Promise<{ success: boolean; skipped: boolean }> => { - const needsAvatarUpload = !!avatarImageUri && isLocalFileUri(avatarImageUri) - const isPreSignedUrlReady = - !avatarUploadUrlLoading && - !!avatarUploadUrlResponse?.preSignedUrl && - !!avatarUploadUrlResponse?.s3UploadFields - const shouldTryAvatarUpload = needsAvatarUpload && isPreSignedUrlReady - - if (!shouldTryAvatarUpload) { - // Return success=true if no upload needed, false if upload needed but can't make request - return { success: !needsAvatarUpload, skipped: true } - } - - const avatarUploadResult = await uploadFileToS3(avatarImageUri, { - preSignedUrl: avatarUploadUrlResponse.preSignedUrl, - s3UploadFields: avatarUploadUrlResponse.s3UploadFields, - }) - - return { ...avatarUploadResult, skipped: false } -} diff --git a/apps/mobile/src/features/unitags/constants.ts b/apps/mobile/src/features/unitags/constants.ts new file mode 100644 index 00000000000..edfe6ca6163 --- /dev/null +++ b/apps/mobile/src/features/unitags/constants.ts @@ -0,0 +1 @@ +export const UNITAG_SUFFIX = '.uni.eth' diff --git a/apps/mobile/src/features/wallet/hooks.ts b/apps/mobile/src/features/wallet/hooks.ts index a00f3002a7a..b8a8c12a1e5 100644 --- a/apps/mobile/src/features/wallet/hooks.ts +++ b/apps/mobile/src/features/wallet/hooks.ts @@ -2,13 +2,13 @@ import { useCallback, useEffect, useState } from 'react' import { useAppSelector } from 'src/app/hooks' import { openModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' +import { ModalName } from 'src/features/telemetry/constants' import { logger } from 'utilities/src/logger/logger' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { useAppDispatch } from 'wallet/src/state' -import { ModalName } from 'wallet/src/telemetry/constants' export function useWalletRestore(params?: { openModalImmediately?: boolean }): { walletNeedsRestore: undefined | boolean diff --git a/apps/mobile/src/features/walletConnect/saga.ts b/apps/mobile/src/features/walletConnect/saga.ts index 2a745ba7320..4f5f5c49cdf 100644 --- a/apps/mobile/src/features/walletConnect/saga.ts +++ b/apps/mobile/src/features/walletConnect/saga.ts @@ -25,7 +25,7 @@ import { import { call, fork, put, take } from 'typed-redux-saga' import { logger } from 'utilities/src/logger/logger' import { config } from 'wallet/src/config' -import { ALL_SUPPORTED_CHAIN_IDS, CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ALL_SUPPORTED_CHAIN_IDS, ChainId, CHAIN_INFO } from 'wallet/src/constants/chains' import { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { EthEvent, EthMethod } from 'wallet/src/features/walletConnect/types' import i18n from 'wallet/src/i18n/i18n' diff --git a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts index b1bbd488448..ae0ca55f64f 100644 --- a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts +++ b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts @@ -6,8 +6,8 @@ import { ChainId } from 'wallet/src/constants/chains' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { - SendTransactionParams, sendTransaction, + SendTransactionParams, } from 'wallet/src/features/transactions/sendTransactionSaga' import { TransactionType } from 'wallet/src/features/transactions/types' import { Account } from 'wallet/src/features/wallet/accounts/types' diff --git a/apps/mobile/src/features/walletConnect/useWalletConnect.ts b/apps/mobile/src/features/walletConnect/useWalletConnect.ts index 1deb339763c..5a33c7548f0 100644 --- a/apps/mobile/src/features/walletConnect/useWalletConnect.ts +++ b/apps/mobile/src/features/walletConnect/useWalletConnect.ts @@ -3,6 +3,7 @@ import { useAppSelector } from 'src/app/hooks' import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { AppModalState } from 'src/features/modals/ModalsState' import { selectModalState } from 'src/features/modals/selectModalState' +import { ModalName } from 'src/features/telemetry/constants' import { makeSelectSessions, selectHasPendingSessionError, @@ -14,7 +15,6 @@ import { WalletConnectRequest, WalletConnectSession, } from 'src/features/walletConnect/walletConnectSlice' -import { ModalName } from 'wallet/src/telemetry/constants' interface WalletConnect { sessions: WalletConnectSession[] diff --git a/apps/mobile/src/screens/DevScreen.tsx b/apps/mobile/src/screens/DevScreen.tsx index 15d06bf2964..932918dc635 100644 --- a/apps/mobile/src/screens/DevScreen.tsx +++ b/apps/mobile/src/screens/DevScreen.tsx @@ -3,15 +3,15 @@ import { I18nManager, ScrollView } from 'react-native' import { useAppDispatch } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' import { BackButton } from 'src/components/buttons/BackButton' +import { Switch } from 'src/components/buttons/Switch' import { Screen } from 'src/components/layout/Screen' +import { resetDismissedWarnings } from 'src/features/tokens/tokensSlice' import { Screens } from 'src/screens/Screens' import { Flex, Text, TouchableArea, useDeviceInsets } from 'ui/src' import { spacing } from 'ui/src/theme' import { logger } from 'utilities/src/logger/logger' -import { Switch } from 'wallet/src/components/buttons/Switch' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { resetDismissedWarnings } from 'wallet/src/features/tokens/tokensSlice' import { createAccountActions } from 'wallet/src/features/wallet/create/createAccountSaga' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' import { resetWallet } from 'wallet/src/features/wallet/slice' diff --git a/apps/mobile/src/screens/ExploreScreen.tsx b/apps/mobile/src/screens/ExploreScreen.tsx index ce4e7659ae9..23fe4a18181 100644 --- a/apps/mobile/src/screens/ExploreScreen.tsx +++ b/apps/mobile/src/screens/ExploreScreen.tsx @@ -9,18 +9,19 @@ import { useExploreStackNavigation } from 'src/app/navigation/types' import { ExploreSections } from 'src/components/explore/ExploreSections' import { SearchEmptySection } from 'src/components/explore/search/SearchEmptySection' import { SearchResultsSection } from 'src/components/explore/search/SearchResultsSection' +import { SearchTextInput } from 'src/components/input/SearchTextInput' import { Screen } from 'src/components/layout/Screen' import { VirtualizedList } from 'src/components/layout/VirtualizedList' +import { useBottomSheetContext } from 'src/components/modals/BottomSheetContext' +import { HandleBar } from 'src/components/modals/HandleBar' import { useReduxModalBackHandler } from 'src/features/modals/hooks' import { selectModalState } from 'src/features/modals/selectModalState' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { ModalName, SectionName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' -import { AnimatedFlex, ColorTokens, Flex, flexStyles, useIsDarkMode } from 'ui/src' +import { AnimatedFlex, ColorTokens, Flex, flexStyles } from 'ui/src' import { useDebounce } from 'utilities/src/time/timing' -import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' -import { HandleBar } from 'wallet/src/components/modals/HandleBar' -import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput' -import { ModalName, SectionName } from 'wallet/src/telemetry/constants' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' export function ExploreScreen(): JSX.Element { const modalInitialState = useAppSelector(selectModalState(ModalName.Explore)).initialState diff --git a/apps/mobile/src/screens/ExternalProfileScreen.tsx b/apps/mobile/src/screens/ExternalProfileScreen.tsx index 27c8081043c..2e26343d28f 100644 --- a/apps/mobile/src/screens/ExternalProfileScreen.tsx +++ b/apps/mobile/src/screens/ExternalProfileScreen.tsx @@ -8,16 +8,16 @@ import { ActivityTab } from 'src/components/home/ActivityTab' import { NftsTab } from 'src/components/home/NftsTab' import { TokensTab } from 'src/components/home/TokensTab' import { Screen } from 'src/components/layout/Screen' -import { renderTabLabel, TAB_STYLES, TabContentProps } from 'src/components/layout/TabHelpers' +import { renderTabLabel, TabContentProps, TAB_STYLES } from 'src/components/layout/TabHelpers' import Trace from 'src/components/Trace/Trace' import TraceTabView from 'src/components/Trace/TraceTabView' import { ProfileHeader } from 'src/features/externalProfile/ProfileHeader' +import { SectionName } from 'src/features/telemetry/constants' import { ExploreModalAwareView } from 'src/screens/ModalAwareView' import { Screens } from 'src/screens/Screens' import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' import { spacing } from 'ui/src/theme' import { useDisplayName } from 'wallet/src/features/wallet/hooks' -import { SectionName, SectionNameType } from 'wallet/src/telemetry/constants' type Props = NativeStackScreenProps & { renderedInModal?: boolean @@ -76,7 +76,7 @@ export function ExternalProfileScreen({ route, }: { route: { - key: SectionNameType + key: SectionName title: string } }) => { diff --git a/apps/mobile/src/screens/FiatOnRampConnecting.tsx b/apps/mobile/src/screens/FiatOnRampConnecting.tsx deleted file mode 100644 index f90099fe41a..00000000000 --- a/apps/mobile/src/screens/FiatOnRampConnecting.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query/react' -import React, { useCallback, useEffect, useState } from 'react' - -import { useTranslation } from 'react-i18next' -import { useAppDispatch } from 'src/app/hooks' -import { Loader } from 'src/components/loading' -import { useTimeout } from 'utilities/src/time/timing' -import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' - -import { NativeStackScreenProps } from '@react-navigation/native-stack' -import { FiatOnRampStackParamList } from 'src/app/navigation/types' -import { - FiatOnRampConnectingView, - SERVICE_PROVIDER_ICON_SIZE, -} from 'src/features/fiatOnRamp/FiatOnRampConnecting' -import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' -import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks' -import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/meldUtils' -import { FiatOnRampScreens } from 'src/screens/Screens' -import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { useFiatOnRampAggregatorWidgetQuery } from 'wallet/src/features/fiatOnRamp/api' -import { pushNotification } from 'wallet/src/features/notifications/slice' -import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { openUri } from 'wallet/src/utils/linking' - -// Design decision -const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS - -type Props = NativeStackScreenProps - -export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | null { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const { addFiatSymbolToNumber } = useLocalizationContext() - const [timeoutElapsed, setTimeoutElapsed] = useState(false) - const activeAccountAddress = useActiveAccountAddressWithThrow() - - const { externalTransactionId, dispatchAddTransaction } = - useFiatOnRampTransactionCreator(activeAccountAddress) - const { selectedQuote, serviceProviders, countryCode, baseCurrencyInfo, quoteCurrency, amount } = - useFiatOnRampContext() - const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders) - - const onError = useCallback((): void => { - dispatch( - pushNotification({ - type: AppNotificationType.Error, - errorMessage: t('Something went wrong.'), - }) - ) - navigation.goBack() - }, [dispatch, navigation, t]) - - const { - data: widgetData, - isLoading: widgetLoading, - error: widgetError, - } = useFiatOnRampAggregatorWidgetQuery( - serviceProvider && quoteCurrency?.currencyInfo?.currency.symbol && baseCurrencyInfo && amount - ? { - serviceProvider: serviceProvider.serviceProvider, - countryCode, - destinationCurrencyCode: quoteCurrency?.currencyInfo?.currency.symbol, - sourceAmount: amount, - sourceCurrencyCode: baseCurrencyInfo.code, - walletAddress: activeAccountAddress, - externalCustomerId: activeAccountAddress, - externalSessionId: externalTransactionId, - } - : skipToken - ) - - useTimeout(() => { - setTimeoutElapsed(true) - }, CONNECTING_TIMEOUT) - - useEffect(() => { - if (!baseCurrencyInfo || !serviceProvider || widgetError) { - onError() - return - } - if (timeoutElapsed && !widgetLoading && widgetData) { - navigation.goBack() - openUri(widgetData.widgetUrl).catch(onError) - // TODO: Uncomment this when https://linear.app/uniswap/issue/MOB-2585/implement-polling-of-transaction-once-user-has-checked-out is implmented - // dispatchAddTransaction() - } - }, [ - navigation, - timeoutElapsed, - widgetData, - widgetLoading, - widgetError, - onError, - dispatchAddTransaction, - baseCurrencyInfo, - serviceProvider, - ]) - - return baseCurrencyInfo && serviceProvider ? ( - - } - serviceProviderName={serviceProvider.name} - /> - ) : null -} diff --git a/apps/mobile/src/screens/FiatOnRampScreen.tsx b/apps/mobile/src/screens/FiatOnRampScreen.tsx deleted file mode 100644 index d174a5ba20a..00000000000 --- a/apps/mobile/src/screens/FiatOnRampScreen.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React, { ComponentProps, useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { TextInput, TextInputProps } from 'react-native' -import { - FadeIn, - FadeOut, - FadeOutDown, - useAnimatedStyle, - useSharedValue, - withSpring, -} from 'react-native-reanimated' -import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks' -import { FiatOnRampStackParamList } from 'src/app/navigation/types' -import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton' -import { Screen } from 'src/components/layout/Screen' -import { FiatOnRampAggregatorTokenSelector } from 'src/features/fiatOnRamp/FiatOnRampAggregatorTokenSelector' -import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection' -import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' -import { FiatOnRampCountryListModal } from 'src/features/fiatOnRamp/FiatOnRampCountryListModal' -import { FiatOnRampCountryPicker } from 'src/features/fiatOnRamp/FiatOnRampCountryPicker' -import { - useFiatOnRampQuotes, - useMeldFiatCurrencySupportInfo, - useParseMeldError, -} from 'src/features/fiatOnRamp/meldHooks' -import { FiatOnRampCurrency, InitialQuoteSelection } from 'src/features/fiatOnRamp/types' -import { sendMobileAnalyticsEvent } from 'src/features/telemetry' -import { MobileEventName } from 'src/features/telemetry/constants' -import { MobileEventProperties } from 'src/features/telemetry/types' -import { FiatOnRampScreens } from 'src/screens/Screens' -import { AnimatedFlex, Flex, Text, useDeviceDimensions } from 'ui/src' -import { usePrevious } from 'utilities/src/react/hooks' -import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy' -import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' -import { HandleBar } from 'wallet/src/components/modals/HandleBar' -import { useFiatOnRampAggregatorServiceProvidersQuery } from 'wallet/src/features/fiatOnRamp/api' -import { MeldTransaction } from 'wallet/src/features/fiatOnRamp/meld' -import { FORQuote } from 'wallet/src/features/fiatOnRamp/types' -import { pushNotification } from 'wallet/src/features/notifications/slice' -import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { ANIMATE_SPRING_CONFIG } from 'wallet/src/features/transactions/utils' - -type Props = NativeStackScreenProps - -function selectInitialQuote( - quotes: FORQuote[] | undefined, - lastTransaction: MeldTransaction | undefined -): { quote: FORQuote | undefined; type: InitialQuoteSelection | undefined } { - if (lastTransaction) { - // setting "Recently used" - // TODO:https://linear.app/uniswap/issue/MOB-2533/implement-recently-used-logic - } else { - // setting "Best overall" - const initialQuote = quotes && quotes.length && quotes[0] - if (initialQuote) { - return { - quote: quotes.reduce((prev, curr) => { - return curr.destinationAmount > prev.destinationAmount ? curr : prev - }, initialQuote), - type: InitialQuoteSelection.Best, - } - } - } - return { quote: undefined, type: undefined } -} - -export function FiatOnRampScreen({ navigation }: Props): JSX.Element { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const { fullWidth } = useDeviceDimensions() - const [selection, setSelection] = useState() - const [value, setValue] = useState('') - const [showTokenSelector, setShowTokenSelector] = useState(false) - const inputRef = useRef(null) - const [selectingCountry, setSelectingCountry] = useState(false) - - const { isSheetReady } = useBottomSheetContext() - - const { - selectedQuote, - setSelectedQuote, - setQuotesSections, - countryCode, - setCountryCode, - amount, - setAmount, - setBaseCurrencyInfo, - setServiceProviders, - quoteCurrency, - setQuoteCurrency, - } = useFiatOnRampContext() - - const resetSelection = (start: number, end?: number): void => { - setSelection({ start, end: end ?? start }) - } - - const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = - useShouldShowNativeKeyboard() - - const { appFiatCurrencySupportedInMeld, meldSupportedFiatCurrency } = - useMeldFiatCurrencySupportInfo() - - const { - error: quotesError, - loading: quotesLoading, - quotes, - } = useFiatOnRampQuotes({ - baseCurrencyAmount: amount, - baseCurrencyCode: meldSupportedFiatCurrency.code, - quoteCurrencyCode: quoteCurrency.currencyInfo?.currency.symbol, - countryCode, - }) - - const { - currentData: serviceProvidersResponse, - isFetching: serviceProvidersLoading, - error: serviceProvidersError, - } = useFiatOnRampAggregatorServiceProvidersQuery() - - const { errorText, errorColor } = useParseMeldError(quotesError || serviceProvidersError) - - const prevQuotes = usePrevious(quotes) - useEffect(() => { - if (quotes && (!selectedQuote || prevQuotes !== quotes)) { - const { quote, type } = selectInitialQuote(quotes, undefined) - if (!quote) { - return - } - const otherQuotes = quotes.filter((item) => item !== quote) - setQuotesSections([ - { data: [quote], type }, - ...(otherQuotes.length ? [{ data: otherQuotes }] : []), - ]) - setSelectedQuote(quote) - } - }, [prevQuotes, quotes, selectedQuote, setQuotesSections, setSelectedQuote, t]) - - useEffect(() => { - if (!quotes && (quotesError || serviceProvidersError || !amount)) { - setQuotesSections(undefined) - setSelectedQuote(undefined) - } - }, [amount, quotesError, serviceProvidersError, quotes, setQuotesSections, setSelectedQuote]) - - const onSelectCountry: ComponentProps['onSelectCountry'] = ( - country - ): void => { - dispatch( - pushNotification({ - type: AppNotificationType.ChooseCountry, - countryName: country.displayName, - countryCode: country.countryCode, - }) - ) - setSelectingCountry(false) - setCountryCode(country.countryCode) - } - - const onChangeValue = - (source: MobileEventProperties[MobileEventName.FiatOnRampAmountEntered]['source']) => - (newAmount: string): void => { - sendMobileAnalyticsEvent(MobileEventName.FiatOnRampAmountEntered, { - source, - }) - setValue(newAmount) - setAmount(newAmount ? parseFloat(newAmount) : 0) - } - - // hide keyboard when user goes to token selector screen - useEffect(() => { - if (showTokenSelector) { - inputRef.current?.blur() - } else if (showNativeKeyboard) { - inputRef.current?.focus() - } - }, [showNativeKeyboard, showTokenSelector]) - - // we only show loading when there are no errors and quote value is not empty - const buttonDisabled = - serviceProvidersLoading || - !!serviceProvidersError || - quotesLoading || - !!quotesError || - !selectedQuote?.destinationAmount - - const screenXOffset = useSharedValue(showTokenSelector ? -fullWidth : 0) - useEffect(() => { - const screenOffset = showTokenSelector ? 1 : 0 - screenXOffset.value = withSpring(-(fullWidth * screenOffset), ANIMATE_SPRING_CONFIG) - }, [screenXOffset, showTokenSelector, fullWidth]) - const wrapperStyle = useAnimatedStyle(() => ({ - transform: [{ translateX: screenXOffset.value }], - })) - - const onContinue = (): void => { - if ( - quotes && - serviceProvidersResponse?.serviceProviders && - serviceProvidersResponse?.serviceProviders.length > 0 && - quoteCurrency?.currencyInfo?.currency - ) { - setBaseCurrencyInfo(meldSupportedFiatCurrency) - setServiceProviders(serviceProvidersResponse.serviceProviders) - navigation.navigate(FiatOnRampScreens.ServiceProviders) - } - } - - return ( - - - - {isSheetReady && ( - - - {t('Buy')} - { - setSelectingCountry(true) - }} - /> - - { - setShowTokenSelector(true) - }} - /> - - {!showNativeKeyboard && ( - - )} - - - - )} - {showTokenSelector && countryCode && ( - setShowTokenSelector(false)} - onSelectCurrency={(newCurrency: FiatOnRampCurrency): void => { - setQuoteCurrency(newCurrency) - setShowTokenSelector(false) - }} - /> - )} - - {Boolean(selectingCountry) && countryCode && ( - { - setSelectingCountry(false) - }} - onSelectCountry={onSelectCountry} - /> - )} - - ) -} diff --git a/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx b/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx deleted file mode 100644 index fd943e1cab1..00000000000 --- a/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { BottomSheetSectionList } from '@gorhom/bottom-sheet' -import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React from 'react' -import { useTranslation } from 'react-i18next' -import { ListRenderItemInfo, SectionListData } from 'react-native' -import { FadeIn, FadeOut } from 'react-native-reanimated' -import { FiatOnRampStackParamList } from 'src/app/navigation/types' -import { BackButton } from 'src/components/buttons/BackButton' -import { FORQuoteItem } from 'src/components/fiatOnRamp/QuoteItem' -import { Screen } from 'src/components/layout/Screen' -import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' -import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/meldUtils' -import { InitialQuoteSelection } from 'src/features/fiatOnRamp/types' -import { MobileEventName } from 'src/features/telemetry/constants' -import { FiatOnRampScreens } from 'src/screens/Screens' -import { AnimatedFlex, Button, Flex, Icons, Inset, Separator, Text } from 'ui/src' -import { Trace } from 'utilities/src/telemetry/trace/Trace' -import { HandleBar } from 'wallet/src/components/modals/HandleBar' -import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' -import { FORQuote } from 'wallet/src/features/fiatOnRamp/types' -import { ElementName } from 'wallet/src/telemetry/constants' - -type Props = NativeStackScreenProps - -const key = (item: FORQuote): string => item.serviceProvider - -export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Element { - const { t } = useTranslation() - const { - selectedQuote, - setSelectedQuote, - quotesSections, - quoteCurrency, - baseCurrencyInfo, - serviceProviders, - } = useFiatOnRampContext() - - const renderItem = ({ item }: ListRenderItemInfo): JSX.Element => { - return ( - - {baseCurrencyInfo && ( - { - setSelectedQuote(item) - }} - /> - )} - - ) - } - - const renderSectionHeader = ({ - section: { type }, - }: { - section: SectionListData - }): JSX.Element => { - return ( - - {type ? ( - - - - {type === InitialQuoteSelection.Best ? t('Best overall') : t('Recently used')} - - - ) : ( - - - - {t('Other options')} - - - - )} - - ) - } - - const onContinue = (): void => { - const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders) - if (serviceProvider) { - navigation.navigate(FiatOnRampScreens.Connecting) - } - } - - return ( - - - - - - - {t('Checkout with')} - - - - - - } - ListFooterComponent={} - focusHook={useBottomSheetFocusHook} - keyExtractor={key} - keyboardDismissMode="on-drag" - keyboardShouldPersistTaps="always" - renderItem={renderItem} - renderSectionHeader={renderSectionHeader} - sections={quotesSections ?? []} - showsVerticalScrollIndicator={false} - stickySectionHeadersEnabled={false} - windowSize={5} - /> - - - - - - - - ) -} diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 84631b08e59..4b42817365c 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -8,9 +8,9 @@ import { useTranslation } from 'react-i18next' import { FlatList, StyleProp, View, ViewProps, ViewStyle } from 'react-native' import { TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler' import Animated, { + cancelAnimation, FadeIn, FadeOut, - cancelAnimation, interpolateColor, runOnJS, useAnimatedGestureHandler, @@ -25,32 +25,39 @@ import { SceneRendererProps, TabBar } from 'react-native-tab-view' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { NavBar, SWAP_BUTTON_HEIGHT } from 'src/app/navigation/NavBar' import { AppStackScreenProp } from 'src/app/navigation/types' -import { ScannerModalState } from 'src/components/QRCodeScanner/constants' -import Trace from 'src/components/Trace/Trace' -import TraceTabView from 'src/components/Trace/TraceTabView' import { AccountHeader } from 'src/components/accounts/AccountHeader' import { pulseAnimation } from 'src/components/buttons/utils' -import { ACTIVITY_TAB_DATA_DEPENDENCIES, ActivityTab } from 'src/components/home/ActivityTab' -import { FEED_TAB_DATA_DEPENDENCIES, FeedTab } from 'src/components/home/FeedTab' -import { NFTS_TAB_DATA_DEPENDENCIES, NftsTab } from 'src/components/home/NftsTab' -import { TOKENS_TAB_DATA_DEPENDENCIES, TokensTab } from 'src/components/home/TokensTab' +import { ActivityTab, ACTIVITY_TAB_DATA_DEPENDENCIES } from 'src/components/home/ActivityTab' +import { FeedTab, FEED_TAB_DATA_DEPENDENCIES } from 'src/components/home/FeedTab' +import { NftsTab, NFTS_TAB_DATA_DEPENDENCIES } from 'src/components/home/NftsTab' +import { TokensTab, TOKENS_TAB_DATA_DEPENDENCIES } from 'src/components/home/TokensTab' import { Screen } from 'src/components/layout/Screen' import { HeaderConfig, + renderTabLabel, ScrollPair, + TabContentProps, TAB_BAR_HEIGHT, TAB_STYLES, TAB_VIEW_SCROLL_THROTTLE, - TabContentProps, - renderTabLabel, useScrollSync, } from 'src/components/layout/TabHelpers' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { TokenBalanceListRow } from 'src/components/TokenBalanceList/TokenBalanceListContext' +import Trace from 'src/components/Trace/Trace' +import TraceTabView from 'src/components/Trace/TraceTabView' import { UnitagBanner } from 'src/components/unitags/UnitagBanner' import { apolloClient } from 'src/data/usePersistedApolloClient' import { PortfolioBalance } from 'src/features/balances/PortfolioBalance' import { openModal } from 'src/features/modals/modalSlice' import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen' -import { MobileEventName } from 'src/features/telemetry/constants' +import { useSelectAddressHasNotifications } from 'src/features/notifications/hooks' +import { + ElementName, + MobileEventName, + ModalName, + SectionName, +} from 'src/features/telemetry/constants' import { useHeartbeatReporter, useLastBalancesReporter } from 'src/features/telemetry/hooks' import { useWalletRestore } from 'src/features/wallet/hooks' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' @@ -66,7 +73,7 @@ import { useMedia, useSporeColors, } from 'ui/src' -import ReceiveIcon from 'ui/src/assets/icons/arrow-down-circle.svg' +import ReceiveIcon from 'ui/src/assets/icons/arrow-down-circle-filled.svg' import BuyIcon from 'ui/src/assets/icons/buy.svg' import ScanIcon from 'ui/src/assets/icons/scan-home.svg' import SendIcon from 'ui/src/assets/icons/send-action.svg' @@ -75,20 +82,10 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useInterval, useTimeout } from 'utilities/src/time/timing' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' -import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks' import { setNotificationStatus } from 'wallet/src/features/notifications/slice' -import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' -import { useUnitagUpdater } from 'wallet/src/features/unitags/context' import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' -import { - ElementName, - ElementNameType, - ModalName, - SectionName, - SectionNameType, -} from 'wallet/src/telemetry/constants' import { HomeScreenTabIndex } from './HomeScreenTabIndex' const CONTENT_HEADER_HEIGHT_ESTIMATE = 270 @@ -132,7 +129,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen const feedTitle = t('Feed') const routes = useMemo(() => { - const tabs: Array<{ key: SectionNameType; title: string }> = [ + const tabs = [ { key: SectionName.HomeTokensTab, title: tokensTitle }, { key: SectionName.HomeNFTsTab, title: nftsTitle }, { key: SectionName.HomeActivityTab, title: activityTitle }, @@ -381,20 +378,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen ] ) - const { refetchUnitagsCounter } = useUnitagUpdater() - const { canClaimUnitag, refetch: refetchCanActiveAddressClaimUnitag } = - useCanActiveAddressClaimUnitag() - - // Force refetch of canClaimUnitag if refetchUnitagsCounter changes - useEffect(() => { - refetchCanActiveAddressClaimUnitag?.() - }, [refetchUnitagsCounter, refetchCanActiveAddressClaimUnitag]) - - const shouldPromptUnitag = - activeAccount.type === AccountType.SignerMnemonic && - activeAccount.skippedUnitagPrompt !== true && - canClaimUnitag - + const hasClaimEligibility = useCanActiveAddressClaimUnitag() const viewOnlyLabel = t('This is a view-only wallet') const contentHeader = useMemo(() => { return ( @@ -414,7 +398,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen )} - {shouldPromptUnitag && ( + {hasClaimEligibility && ( @@ -424,10 +408,10 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen }, [ activeAccount.address, isSignerAccount, + viewOnlyLabel, actions, + hasClaimEligibility, onPressViewOnlyLabel, - viewOnlyLabel, - shouldPromptUnitag, ]) const contentContainerStyle = useMemo>( @@ -561,7 +545,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen route, }: { route: { - key: SectionNameType + key: SectionName title: string } }) => { @@ -684,7 +668,7 @@ type QuickAction = { eventName?: MobileEventName iconScale?: number label: string - name: ElementNameType + name: ElementName sentryLabel: string onPress: () => void } @@ -719,7 +703,7 @@ function ActionButton({ iconScale = 1, }: { eventName?: MobileEventName - name: ElementNameType + name: ElementName label: string Icon: React.FC onPress: () => void diff --git a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx index 85e650eb5b4..eb7891f6c0a 100644 --- a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx +++ b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx @@ -8,11 +8,12 @@ import Trace from 'src/components/Trace/Trace' import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { OptionCard } from 'src/features/onboarding/OptionCard' +import { ElementName } from 'src/features/telemetry/constants' import { OnboardingScreens } from 'src/screens/Screens' +import { openSettings } from 'src/utils/linking' import { useAddBackButton } from 'src/utils/useAddBackButton' import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import EyeIcon from 'ui/src/assets/icons/eye.svg' -import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' import { AppTFunction } from 'ui/src/i18n/types' import { iconSizes } from 'ui/src/theme' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' @@ -20,8 +21,6 @@ import { PendingAccountActions, pendingAccountActions, } from 'wallet/src/features/wallet/create/pendingAccountsSaga' -import { ElementName, ElementNameType } from 'wallet/src/telemetry/constants' -import { openSettings } from 'wallet/src/utils/linking' import { isAndroid } from 'wallet/src/utils/platform' interface ImportMethodOption { @@ -30,7 +29,7 @@ interface ImportMethodOption { icon: React.ReactNode nav: OnboardingScreens importType: ImportType - name: ElementNameType + name: ElementName } const options: ImportMethodOption[] = [ @@ -60,7 +59,6 @@ type Props = NativeStackScreenProps - - {importOptions.map(({ title, blurb, icon, nav, importType, name }, i) => ( + + + {importOptions.map(({ title, blurb, icon, nav, importType, name }) => ( - + + title={ + isAndroid + ? t('Enter your Google Drive backup password') + : t('Enter your iCloud backup password') + }> b.createdAt - a.createdAt) @@ -49,7 +48,7 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop ? t('There are multiple recovery phrases backed up to your Google Drive.') : t('There are multiple recovery phrases backed up to your iCloud.') } - title={t('Select a backup to restore')}> + title={t('Select backup to restore')}> {sortedBackups.map((backup) => { @@ -57,13 +56,11 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop return ( => onPressRestoreBackup(backup)}> @@ -73,16 +70,11 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop {sanitizeAddressText(shortenAddress(mnemonicId))} - {dayjs.unix(createdAt).format('MMM D, YYYY [at] h:mma')} + {dayjs.unix(createdAt).format('MMM D, YYYY, h:mma')} - + ) diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.test.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.test.tsx index 37cb28d298e..ee21fad6eb8 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.test.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.test.tsx @@ -23,7 +23,7 @@ const routeProp = { params: { importType: ImportType.CreateNew } } as RouteProp< > describe(SeedPhraseInputScreen, () => { - it.skip('seed phrase initial screen rendering', async () => { + it('seed phrase initial screen rendering', async () => { const tree = render() expect(tree.toJSON()).toMatchSnapshot() diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx index e205db7c06b..8f983f2f6dc 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx @@ -7,9 +7,11 @@ import Trace from 'src/components/Trace/Trace' import { useLockScreenOnBlur } from 'src/features/authentication/lockScreenContext' import { GenericImportForm } from 'src/features/import/GenericImportForm' import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { ElementName } from 'src/features/telemetry/constants' import { OnboardingScreens } from 'src/screens/Screens' +import { openUri } from 'src/utils/linking' import { useAddBackButton } from 'src/utils/useAddBackButton' -import { Button, Flex, Icons, Text, TouchableArea } from 'ui/src' +import { Button, Flex, Text, TouchableArea } from 'ui/src' import { uniswapUrls } from 'wallet/src/constants/urls' import { ImportType } from 'wallet/src/features/onboarding/types' import { useNonPendingSignerAccounts } from 'wallet/src/features/wallet/hooks' @@ -17,8 +19,6 @@ import { importAccountActions } from 'wallet/src/features/wallet/import/importAc import { ImportAccountType } from 'wallet/src/features/wallet/import/types' import { NUMBER_OF_WALLETS_TO_IMPORT } from 'wallet/src/features/wallet/import/utils' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { ElementName } from 'wallet/src/telemetry/constants' -import { openUri } from 'wallet/src/utils/linking' import { MnemonicValidationError, translateMnemonicErrorMessage, @@ -44,6 +44,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): useLockScreenOnBlur(pastePermissionModalOpen) const [value, setValue] = useState(undefined) + const [showSuccess, setShowSuccess] = useState(false) const [errorMessage, setErrorMessage] = useState(undefined) const isRestoringMnemonic = params.importType === ImportType.RestoreMnemonic @@ -87,12 +88,20 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): const onBlur = useCallback(() => { const { error, invalidWord } = validateMnemonic(value) if (error) { + setShowSuccess(false) setErrorMessage(translateMnemonicErrorMessage(error, invalidWord, t)) } }, [t, value]) const onChange = (text: string | undefined): void => { - const { error, invalidWord } = validateSetOfWords(text) + const { error, invalidWord, isValidLength } = validateSetOfWords(text) + + // always show success UI if phrase is valid length + if (isValidLength) { + setShowSuccess(true) + } else { + setShowSuccess(false) + } // suppress error messages if the user is not done typing a word const suppressError = @@ -123,30 +132,24 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): : t('Your recovery phrase will only be stored locally on your device.') } title={isRestoringMnemonic ? t('No backups found') : t('Enter your recovery phrase')}> - - - setPastePermissionModalOpen(false)} - beforePasteButtonPress={(): void => setPastePermissionModalOpen(true)} - errorMessage={errorMessage} - inputAlignment="flex-start" - placeholderLabel={t('Type your recovery phrase')} - textAlign="left" - value={value} - onBlur={onBlur} - onChange={onChange} - /> - + + setPastePermissionModalOpen(false)} + beforePasteButtonPress={(): void => setPastePermissionModalOpen(true)} + errorMessage={errorMessage} + placeholderLabel={t('Enter recovery phrase')} + showSuccess={showSuccess} + value={value} + onBlur={onBlur} + onChange={onChange} + /> - - + {isRestoringMnemonic ? t('Try searching again') : t('How do I find my recovery phrase?')} @@ -155,6 +158,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): + )} @@ -183,25 +175,3 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem ) } - -function RecoveryPhraseTooltip({ - onPressEducationButton, -}: { - onPressEducationButton: () => void -}): JSX.Element { - const { t } = useTranslation() - return ( - - - - {t('What is a recovery phrase?')} - - - ) -} diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx index ed1356f76f5..8ebe476e313 100644 --- a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx @@ -30,7 +30,9 @@ export function CloudBackupPasswordCreateScreen({ return ( diff --git a/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx b/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx index db68ef47289..e184510d5bf 100644 --- a/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx @@ -1,16 +1,17 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { ActivityIndicator, TextInput as NativeTextInput, StyleSheet } from 'react-native' +import { ActivityIndicator, StyleSheet, TextInput as NativeTextInput } from 'react-native' import { useAppDispatch } from 'src/app/hooks' import { OnboardingStackParamList } from 'src/app/navigation/types' +import { TextInput } from 'src/components/input/TextInput' import Trace from 'src/components/Trace/Trace' import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { ElementName } from 'src/features/telemetry/constants' import { OnboardingScreens } from 'src/screens/Screens' import { useAddBackButton } from 'src/utils/useAddBackButton' -import { AnimatePresence, Button, Flex, Icons, Text } from 'ui/src' +import { AnimatePresence, Button, Flex, Icons, Text, useMedia } from 'ui/src' import { fonts } from 'ui/src/theme' -import { TextInput } from 'wallet/src/components/input/TextInput' import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts' import { ImportType } from 'wallet/src/features/onboarding/types' import { @@ -23,7 +24,6 @@ import { pendingAccountActions, } from 'wallet/src/features/wallet/create/pendingAccountsSaga' import { usePendingAccounts } from 'wallet/src/features/wallet/hooks' -import { ElementName } from 'wallet/src/telemetry/constants' import { shortenAddress } from 'wallet/src/utils/addresses' import { isAndroid } from 'wallet/src/utils/platform' @@ -64,7 +64,7 @@ export function EditNameScreen({ navigation, route: { params } }: Props): JSX.El navigation.navigate({ name: params?.importType === ImportType.CreateNew - ? OnboardingScreens.WelcomeWallet + ? OnboardingScreens.QRAnimation : OnboardingScreens.Notifications, merge: true, params, @@ -113,6 +113,7 @@ function CustomizationSection({ setAccountName: Dispatch> }): JSX.Element { const { t } = useTranslation() + const media = useMedia() const textInputRef = useRef(null) // we default it to `true` to avoid flickering of a pencil icon, @@ -123,6 +124,8 @@ function CustomizationSection({ textInputRef.current?.focus() } + const inputSize = media.short ? fonts.heading3.fontSize : fonts.heading2.fontSize + return ( - - - - { - setFocused(false) - setAccountName(accountName.trim()) - }} - onChangeText={setAccountName} - onFocus={(): void => setFocused(true)} - /> - - {!focused && ( - + + + {t('Continue')} + + ) diff --git a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx index 87de4e84a56..46207726b9b 100644 --- a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx @@ -11,9 +11,12 @@ import { useBiometricAppSettings } from 'src/features/biometrics/hooks' import { promptPushPermission } from 'src/features/notifications/Onesignal' import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { ElementName } from 'src/features/telemetry/constants' import { OnboardingScreens } from 'src/screens/Screens' -import { Button, Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { openSettings } from 'src/utils/linking' +import { Button, Flex, Text, TouchableArea } from 'ui/src' import { ONBOARDING_NOTIFICATIONS_DARK, ONBOARDING_NOTIFICATIONS_LIGHT } from 'ui/src/assets' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' import { EditAccountAction, @@ -22,8 +25,6 @@ import { import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { selectAccounts } from 'wallet/src/features/wallet/selectors' import i18n from 'wallet/src/i18n/i18n' -import { ElementName } from 'wallet/src/telemetry/constants' -import { openSettings } from 'wallet/src/utils/linking' import { isIOS } from 'wallet/src/utils/platform' type Props = NativeStackScreenProps diff --git a/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx b/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx index f27c500c677..73a7d20f79e 100644 --- a/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx +++ b/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx @@ -11,21 +11,25 @@ import { import { ResizeMode, Video } from 'expo-av' import React, { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { StyleSheet } from 'react-native' +import { StyleProp, StyleSheet, ViewStyle } from 'react-native' import { + AnimateStyle, Easing, EntryExitAnimationFunction, runOnJS, - StyleProps, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated' +import { AddressDisplay } from 'src/components/AddressDisplay' import { GradientBackground } from 'src/components/gradients/GradientBackground' import { UniconThemedGradient } from 'src/components/gradients/UniconThemedGradient' +import { Arrow } from 'src/components/icons/Arrow' import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode' import Trace from 'src/components/Trace/Trace' +import { useUniconColors } from 'src/components/unicons/utils' +import { ElementName } from 'src/features/telemetry/constants' import { flashWipeAnimation, letsGoButtonFadeIn, @@ -38,22 +42,12 @@ import { textSlideUpAtEnd, videoFadeOut, } from 'src/screens/Onboarding/QRAnimation/animations' -import { - Button, - Flex, - Text, - useIsDarkMode, - useMedia, - useSporeColors, - useUniconColors, -} from 'ui/src' +import { Button, Flex, Text, useMedia, useSporeColors } from 'ui/src' import { ONBOARDING_QR_ETCHING_VIDEO_DARK, ONBOARDING_QR_ETCHING_VIDEO_LIGHT } from 'ui/src/assets' import LockIcon from 'ui/src/assets/icons/lock.svg' import { AnimatedFlex, flexStyles } from 'ui/src/components/layout' -import { fonts, iconSizes, opacify, spacing } from 'ui/src/theme' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { Arrow } from 'wallet/src/components/icons/Arrow' -import { ElementName } from 'wallet/src/telemetry/constants' +import { fonts, iconSizes, opacify } from 'ui/src/theme' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' export function QRAnimation({ activeAddress, @@ -87,7 +81,7 @@ export function QRAnimation({ // the config for this animation is defined in the animations.ts file in the same folder as this component, but because of the callback it made more sense to leave the actual animation definition in this file const qrSlideUpAndFadeIn: EntryExitAnimationFunction = () => { 'worklet' - const animations: StyleProps = { + const animations: AnimateStyle> = { opacity: withDelay( qrSlideUpAndFadeInConfig.opacity.delay, withTiming(qrSlideUpAndFadeInConfig.opacity.endValue, { @@ -107,7 +101,9 @@ export function QRAnimation({ }, ], } - const initialValues: StyleProps = { + // > doesn't quite work because of translateY + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const initialValues: AnimateStyle = { opacity: qrSlideUpAndFadeInConfig.opacity.startValue, transform: [{ translateY: qrSlideUpAndFadeInConfig.translateY.startValue }], } @@ -163,7 +159,7 @@ export function QRAnimation({ // used throughout the page the get the size of the QR code container // setting as a constant so that it doesn't get defined by padding and screen size and give us less design control - const QR_CONTAINER_SIZE = media.short ? 175 : 242 + const QR_CONTAINER_SIZE = media.short ? 160 : 242 const QR_CODE_SIZE = media.short ? 140 : 190 const UNICON_SIZE = 64 @@ -183,7 +179,7 @@ export function QRAnimation({ - + @@ -260,35 +256,34 @@ export function QRAnimation({ /> + + + + + - {/* negative top margin required because the glow around the QR code is absolute with -50 margin */} - - - - - - + {t('Welcome to your new wallet')} {isNewWallet @@ -359,7 +354,6 @@ const styles = StyleSheet.create({ flexDirection: 'column', justifyContent: 'center', marginHorizontal: 28, - marginTop: spacing.spacing60, }, video: { bottom: 4, diff --git a/apps/mobile/src/screens/Onboarding/QRAnimation/animations.ts b/apps/mobile/src/screens/Onboarding/QRAnimation/animations.ts index e00627b5d97..7be1406deec 100644 --- a/apps/mobile/src/screens/Onboarding/QRAnimation/animations.ts +++ b/apps/mobile/src/screens/Onboarding/QRAnimation/animations.ts @@ -1,7 +1,7 @@ import { + AnimateStyle, Easing, EntryExitAnimationFunction, - StyleProps, withDelay, withSequence, withTiming, @@ -49,7 +49,7 @@ const qrScaleInConfig = { export const qrScaleIn: EntryExitAnimationFunction = () => { 'worklet' - const animations: StyleProps = { + const animations: AnimateStyle = { transform: [ { scale: withDelay( @@ -62,7 +62,7 @@ export const qrScaleIn: EntryExitAnimationFunction = () => { }, ], } - const initialValues: StyleProps = { + const initialValues: AnimateStyle = { transform: [{ scale: qrScaleInConfig.scale.startValue }], } return { @@ -118,7 +118,7 @@ export const flashWipeConfig = { export const flashWipeAnimation: EntryExitAnimationFunction = () => { 'worklet' - const animations: StyleProps = { + const animations: AnimateStyle = { opacity: withSequence( withDelay( flashWipeConfig.opacityIn.delay, @@ -144,7 +144,7 @@ export const flashWipeAnimation: EntryExitAnimationFunction = () => { }, ], } - const initialValues: StyleProps = { + const initialValues: AnimateStyle = { opacity: flashWipeConfig.opacityIn.startValue, transform: [{ scale: flashWipeConfig.scale.startValue }], } @@ -238,7 +238,7 @@ const textSlideUpAndFadeInConfig = { export const textSlideUpAtEnd: EntryExitAnimationFunction = () => { 'worklet' - const animations: StyleProps = { + const animations: AnimateStyle = { opacity: withDelay( textSlideUpAndFadeInConfig.opacityOut.delay, withTiming(textSlideUpAndFadeInConfig.opacityOut.endValue, { @@ -258,7 +258,7 @@ export const textSlideUpAtEnd: EntryExitAnimationFunction = () => { }, ], } - const initialValues: StyleProps = { + const initialValues: AnimateStyle = { transform: [{ translateY: textSlideUpAndFadeInConfig.translateY.startValue }], opacity: textSlideUpAndFadeInConfig.opacityOut.startValue, } @@ -300,7 +300,7 @@ const qrSlideUpAtEndConfig = { export const qrSlideUpAtEnd: EntryExitAnimationFunction = () => { 'worklet' - const animations: StyleProps = { + const animations: AnimateStyle = { transform: [ { translateY: withDelay( @@ -313,7 +313,7 @@ export const qrSlideUpAtEnd: EntryExitAnimationFunction = () => { }, ], } - const initialValues: StyleProps = { + const initialValues: AnimateStyle = { transform: [{ translateY: qrSlideUpAndFadeInConfig.translateY.startValue }], } return { diff --git a/apps/mobile/src/screens/Onboarding/QRAnimationScreen.tsx b/apps/mobile/src/screens/Onboarding/QRAnimationScreen.tsx new file mode 100644 index 00000000000..b70de3ce56c --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/QRAnimationScreen.tsx @@ -0,0 +1,39 @@ +import { CompositeScreenProps } from '@react-navigation/core' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { AppStackParamList, OnboardingStackParamList } from 'src/app/navigation/types' +import { Screen } from 'src/components/layout/Screen' +import { QRAnimation } from 'src/screens/Onboarding/QRAnimation/QRAnimation' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' + +type Props = CompositeScreenProps< + NativeStackScreenProps, + NativeStackScreenProps +> + +export function QRAnimationScreen({ navigation, route: { params } }: Props): JSX.Element { + const activeAddress = useActiveAccountAddressWithThrow() + + const onPressNext = (): void => { + navigation.navigate({ + name: + params?.importType === ImportType.CreateNew + ? OnboardingScreens.Backup + : OnboardingScreens.Notifications, + merge: true, + params, + }) + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx b/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx index 1431060e7e1..abd0ffc38a7 100644 --- a/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx @@ -20,16 +20,17 @@ import { import { setRequiredForTransactions } from 'src/features/biometrics/slice' import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { ElementName } from 'src/features/telemetry/constants' import { OnboardingScreens } from 'src/screens/Screens' -import { Button, Flex, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' +import { openSettings } from 'src/utils/linking' +import { Button, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { SECURITY_SCREEN_BACKGROUND_DARK, SECURITY_SCREEN_BACKGROUND_LIGHT } from 'ui/src/assets' import FaceIcon from 'ui/src/assets/icons/faceid-thin.svg' import FingerprintIcon from 'ui/src/assets/icons/fingerprint.svg' import { borderRadii, imageSizes } from 'ui/src/theme' +import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { ImportType } from 'wallet/src/features/onboarding/types' -import { ElementName } from 'wallet/src/telemetry/constants' import { opacify } from 'wallet/src/utils/colors' -import { openSettings } from 'wallet/src/utils/linking' import { isIOS } from 'wallet/src/utils/platform' type Props = NativeStackScreenProps diff --git a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx deleted file mode 100644 index 4b200ce49e2..00000000000 --- a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Trans, useTranslation } from 'react-i18next' -import { Text } from 'ui/src' -import { uniswapUrls } from 'wallet/src/constants/urls' -import { openUri } from 'wallet/src/utils/linking' - -export function TermsOfService(): JSX.Element { - const { t } = useTranslation() - return ( - - - By continuing, I agree to the{' '} - => openUri(uniswapUrls.termsOfServiceUrl)}> - Terms of Service - {' '} - and consent to the{' '} - => openUri(uniswapUrls.privacyPolicyUrl)}> - Privacy Policy - - . - - - ) -} diff --git a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx b/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx deleted file mode 100644 index d80062e67ed..00000000000 --- a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { CompositeScreenProps } from '@react-navigation/core' -import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React from 'react' -import { useTranslation } from 'react-i18next' -import { AppStackParamList, OnboardingStackParamList } from 'src/app/navigation/types' -import AnimatedNumber from 'src/components/AnimatedNumber' -import { Screen } from 'src/components/layout/Screen' -import Trace from 'src/components/Trace/Trace' -import { OnboardingScreens, Screens } from 'src/screens/Screens' -import { Button, Flex, Loader, Text, useMedia, useSporeColors } from 'ui/src' -import LockIcon from 'ui/src/assets/icons/lock.svg' -import { fonts, iconSizes, opacify } from 'ui/src/theme' -import { NumberType } from 'utilities/src/format/types' -import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' -import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' -import { Arrow } from 'wallet/src/components/icons/Arrow' -import { useENSAvatar } from 'wallet/src/features/ens/api' -import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { useActiveAccountAddressWithThrow, useDisplayName } from 'wallet/src/features/wallet/hooks' -import { ElementName } from 'wallet/src/telemetry/constants' - -type Props = CompositeScreenProps< - NativeStackScreenProps, - NativeStackScreenProps -> - -export function WelcomeWalletScreen({ navigation, route: { params } }: Props): JSX.Element { - const colors = useSporeColors() - const { t } = useTranslation() - const { convertFiatAmountFormatted } = useLocalizationContext() - const media = useMedia() - - const activeAddress = useActiveAccountAddressWithThrow() - const displayName = useDisplayName(activeAddress) - const { data: avatar } = useENSAvatar(activeAddress) - - const onPressNext = (): void => { - navigation.navigate({ - name: OnboardingScreens.Backup, - merge: true, - params, - }) - } - - const zeroBalance = convertFiatAmountFormatted(0, NumberType.PortfolioBalance) - - return ( - - - - - - - - - - - {t('Welcome to your new wallet')} - - - {t( - 'This is your personal space for tokens, NFTs, and all of your trades. Finish setting it up to keep your funds safe.' - )} - - - - - )} diff --git a/apps/mobile/src/screens/SettingsWalletEdit.tsx b/apps/mobile/src/screens/SettingsWalletEdit.tsx index 3576392a5e4..fd30883eb09 100644 --- a/apps/mobile/src/screens/SettingsWalletEdit.tsx +++ b/apps/mobile/src/screens/SettingsWalletEdit.tsx @@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next' import { Keyboard, KeyboardAvoidingView, StyleSheet } from 'react-native' import { useAppDispatch } from 'src/app/hooks' import { SettingsStackParamList } from 'src/app/navigation/types' +import { TextInput } from 'src/components/input/TextInput' import { BackHeader } from 'src/components/layout/BackHeader' import { Screen } from 'src/components/layout/Screen' import { UnitagBanner } from 'src/components/unitags/UnitagBanner' import { Button, Flex, Icons, Text } from 'ui/src' import { fonts } from 'ui/src/theme' -import { TextInput } from 'wallet/src/components/input/TextInput' import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts' import { ChainId } from 'wallet/src/constants/chains' import { useENS } from 'wallet/src/features/ens/useENS' @@ -19,7 +19,6 @@ import { EditAccountAction, editAccountActions, } from 'wallet/src/features/wallet/accounts/editAccountSaga' -import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' import { shortenAddress } from 'wallet/src/utils/addresses' import { isIOS } from 'wallet/src/utils/platform' @@ -40,8 +39,6 @@ export function SettingsWalletEdit({ const [initialNickname, setInitialNickname] = useState(ensName || activeAccount?.name) const [showEditInput, setShowEditInput] = useState(false) const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) - const showUnitagBanner = - unitagsFeatureFlagEnabled && activeAccount?.type === AccountType.SignerMnemonic const onPressShowEditInput = (): void => { setShowEditInput(true) @@ -77,7 +74,7 @@ export function SettingsWalletEdit({ contentContainerStyle={styles.expand} style={styles.base}> - {t('Edit Label')} + {t('Nickname')}