diff --git a/RELEASE b/RELEASE index 5bb53a18a38..c45fb2d4b50 100644 --- a/RELEASE +++ b/RELEASE @@ -1,10 +1,9 @@ -We are back with some new new updates! Here’s the latest: +Excited to share some new updates! Here’s what’s new: -Support for Blast: We now support the Blast Network! You can now send, receive, swap and more on it. +Expanded Fiat On-ramp Providers — We’ve added more provider options to on-ramp to crypto from your wallet, dependent on your geography. -Extension Beta Waitlist: If you’ve claimed a Username, you’re automatically added to the waitlist of our newest product, the Uniswap Extension (Beta). If you’re off the waitlist, you’ll receive a notification as such, which will allow you access to try out the product. +Other changes: -Other changes: - -- Polish around fiat onramp +- Polish around QR codes +- Improved error reporting - Various bug fixes and performance improvements diff --git a/VERSION b/VERSION index db0f59602de..97eb6715fd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -mobile/1.25 \ No newline at end of file +mobile/1.26 \ No newline at end of file diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 0bd76016b7f..ffab6f26402 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -111,6 +111,7 @@ Add the following to your .rc file Install [Android Studio](https://developer.android.com/studio) Add the following to your .rc file + ``` export ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_HOME/emulator @@ -175,7 +176,7 @@ These are some tools you might want to familiarize yourself with to understand t ## Migrations -We use `redux-persist` to persist Redux state between user sessions. When the Redux state schema is altered, a migration may be needed to transfer the existing persisted state to the new Redux schema. Failing to define a migration results in the app defaulting to the persisted schema, which will very likely cause `undefined` errors because the code has references to Redux state properties that were dropped in favor the the persisted schema. +We use `redux-persist` to persist Redux state between user sessions. When the Redux state schema is altered, a migration may be needed to transfer the existing persisted state to the new Redux schema. Failing to define a migration results in the app defaulting to the persisted schema, which will very likely cause `undefined` errors because the code has references to Redux state properties that were dropped in favor the persisted schema. ### When to define a migration diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 815958611e7..9e8922b2f98 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -131,17 +131,17 @@ android { dev { isDefault(true) applicationIdSuffix ".dev" - versionName "1.25" + versionName "1.26" dimension "variant" } beta { applicationIdSuffix ".beta" - versionName "1.25" + versionName "1.26" dimension "variant" } prod { dimension "variant" - versionName "1.25" + versionName "1.26" } } diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 6f3253a345e..6c5294dd9b6 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -1151,12 +1151,8 @@ PODS: - React-Core - react-native-restart (0.0.27): - React-Core - - react-native-safe-area-context (4.5.0): - - RCT-Folly - - RCTRequired - - RCTTypeSafety + - react-native-safe-area-context (4.9.0): - React-Core - - ReactCommon/turbomodule/core - react-native-skia (0.1.187): - React - React-callinvoker @@ -1749,7 +1745,7 @@ SPEC CHECKSUMS: react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8 react-native-pager-view: 3051346698a0ba0c4e13e40097cc11b00ee03cca react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 - react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc + react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b react-native-skia: e7385e2f5ebe284df53f0def573198fe69a7bd72 react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581 diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj index 2fc2abbec90..45e3fd55870 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -2450,7 +2450,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2496,7 +2496,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; @@ -2542,7 +2542,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; @@ -2588,7 +2588,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; @@ -2630,7 +2630,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2673,7 +2673,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; @@ -2716,7 +2716,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; @@ -2759,7 +2759,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; @@ -2795,7 +2795,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2833,7 +2833,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3003,7 +3003,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3047,7 +3047,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; @@ -3143,7 +3143,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3214,7 +3214,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; @@ -3310,7 +3310,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3381,7 +3381,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.25; + MARKETING_VERSION = 1.26; 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/WidgetsCore/Utils/Network.swift b/apps/mobile/ios/WidgetsCore/Utils/Network.swift index 22f2ff1becc..6a4d3b97c85 100644 --- a/apps/mobile/ios/WidgetsCore/Utils/Network.swift +++ b/apps/mobile/ios/WidgetsCore/Utils/Network.swift @@ -11,7 +11,7 @@ import Apollo public class Network { public static let shared = Network() - private let UNISWAP_API_URL = Env.UNISWAP_API_BASE_URL + "/v1/graphql" + private let UNISWAP_API_URL = "https://ios.wallet.gateway.uniswap.org/v1/graphql" public lazy var apollo: ApolloClient = { let cache = InMemoryNormalizedCache() diff --git a/apps/mobile/jest.config.js b/apps/mobile/jest.config.js index a583b497650..285f047fb4a 100644 --- a/apps/mobile/jest.config.js +++ b/apps/mobile/jest.config.js @@ -4,10 +4,6 @@ const preset = require('../../config/jest-presets/jest/jest-preset') module.exports = { ...preset, preset: 'jest-expo', - transform: { - ...preset.transform, - '^.+\\.jsx?$': 'babel-jest', - }, displayName: 'Mobile Wallet', collectCoverageFrom: [ 'src/**/*.{js,ts,tsx}', diff --git a/apps/mobile/package.json b/apps/mobile/package.json index bbbe4cb5349..73f29d47cb5 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -83,8 +83,8 @@ "@uniswap/analytics": "1.7.0", "@uniswap/analytics-events": "2.32.0", "@uniswap/ethers-rs-mobile": "0.0.5", - "@uniswap/sdk-core": "4.2.0", - "@uniswap/v3-sdk": "3.11.0", + "@uniswap/sdk-core": "4.2.1-beta.1", + "@uniswap/v3-sdk": "3.11.1-beta.2", "@walletconnect/core": "2.11.2", "@walletconnect/react-native-compat": "2.11.2", "@walletconnect/utils": "2.11.2", @@ -163,15 +163,14 @@ "@babel/runtime": "7.18.9", "@faker-js/faker": "7.6.0", "@storybook/react": "7.0.2", - "@tamagui/babel-plugin": "1.94.3", + "@tamagui/babel-plugin": "1.94.5", "@testing-library/react-hooks": "7.0.2", "@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.11.2", - "@welldone-software/why-did-you-render": "7.0.1", - "babel-jest": "29.6.1", + "@welldone-software/why-did-you-render": "8.0.1", "babel-loader": "8.2.3", "babel-plugin-react-native-web": "0.17.5", "babel-plugin-react-require": "4.0.0", diff --git a/apps/mobile/scripts/copy_env_vars_to_swift.py b/apps/mobile/scripts/copy_env_vars_to_swift.py index 0ce7e87355c..735e90550db 100644 --- a/apps/mobile/scripts/copy_env_vars_to_swift.py +++ b/apps/mobile/scripts/copy_env_vars_to_swift.py @@ -3,7 +3,7 @@ ENV_DEFAULTS_FILE = '../../.env.defaults' ENV_DEFAULTS_LOCAL_FILE = '../../.env.defaults.local' SWIFT_FILE_PATH = 'ios/WidgetsCore/Env.swift' -SWIFT_ENV_VARIABLES = ['UNISWAP_API_BASE_URL','UNISWAP_API_KEY'] +SWIFT_ENV_VARIABLES = ['UNISWAP_API_KEY'] def to_swift_constant_line(key, value): return f' static let {key.upper()} = "{value}"' diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index d8684a052e0..6ce4d5f5b2a 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -45,15 +45,14 @@ import { getSentryTracesSamplingRate, getStatsigEnvironmentTier, } from 'src/utils/version' -import { Statsig, StatsigProvider } from 'statsig-react-native' +import { StatsigProvider } from 'statsig-react-native' import { flexStyles, useIsDarkMode } from 'ui/src' import { config } from 'uniswap/src/config' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { - DUMMY_STATSIG_SDK_KEY, - ExperimentsWallet, -} from 'uniswap/src/features/experiments/constants' -import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/experiments/flags' +import { DUMMY_STATSIG_SDK_KEY } from 'uniswap/src/features/gating/constants' +import { WALLET_EXPERIMENTS } from 'uniswap/src/features/gating/experiments' +import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' +import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import i18n from 'uniswap/src/i18n/i18n' import { CurrencyId } from 'uniswap/src/types/currency' @@ -175,12 +174,13 @@ function SentryTags({ children }: PropsWithChildren): JSX.Element { Sentry.setTag(`featureFlag.${flagKey}`, Statsig.checkGateWithExposureLoggingDisabled(flagKey)) } - Object.entries(ExperimentsWallet).map(([_, experimentName]) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_, experimentDef] of WALLET_EXPERIMENTS.entries()) { Sentry.setTag( - `experiment.${experimentName}`, - Statsig.getExperimentWithExposureLoggingDisabled(experimentName).getGroupName() + `experiment.${experimentDef.name}`, + Statsig.getExperimentWithExposureLoggingDisabled(experimentDef.name).getGroupName() ) - }) + } }, []) return <>{children} diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx index f69e2c370e0..ecf38b0f706 100644 --- a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx +++ b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx @@ -6,25 +6,21 @@ 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 { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { logger } from 'utilities/src/logger/logger' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { NavigateToNftItemArgs, - NavigateToSendArgs, + NavigateToSendFlowArgs, NavigateToSwapFlowArgs, ShareNftArgs, ShareTokenArgs, WalletNavigationProvider, + getNavigateToSendFlowArgsInitialState, getNavigateToSwapFlowArgsInitialState, } from 'wallet/src/contexts/WalletNavigationContext' -import { AssetType } from 'wallet/src/entities/assets' import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' import { ModalName, ShareableEntity, WalletEventName } from 'wallet/src/telemetry/constants' import { getNftUrl, getTokenUrl } from 'wallet/src/utils/linking' @@ -122,24 +118,12 @@ function useNavigateToReceive(): () => void { }, [dispatch]) } -function useNavigateToSend(): (args: NavigateToSendArgs) => void { +function useNavigateToSend(): (args: NavigateToSendFlowArgs) => void { const dispatch = useAppDispatch() return useCallback( - (args: NavigateToSendArgs) => { - const initialSendState: TransactionState = { - exactCurrencyField: CurrencyField.INPUT, - exactAmountToken: '', - [CurrencyField.INPUT]: args - ? { - address: args.currencyAddress, - chainId: args.chainId, - type: AssetType.Currency, - } - : null, - [CurrencyField.OUTPUT]: null, - showRecipientSelector: true, - } + (args: NavigateToSendFlowArgs) => { + const initialSendState = getNavigateToSendFlowArgsInitialState(args) dispatch(openModal({ name: ModalName.Send, initialState: initialSendState })) }, [dispatch] diff --git a/apps/mobile/src/app/migrations.test.ts b/apps/mobile/src/app/migrations.test.ts index 2df635ba087..c62de8fb30b 100644 --- a/apps/mobile/src/app/migrations.test.ts +++ b/apps/mobile/src/app/migrations.test.ts @@ -62,6 +62,7 @@ import { v5Schema, v60Schema, v61Schema, + v62Schema, v6Schema, v7Schema, v8Schema, @@ -1390,4 +1391,11 @@ describe('Redux state migrations', () => { expect(v62.behaviorHistory.extensionOnboardingState).toBe(ExtensionOnboardingState.Undefined) }) + + it('migrates from v62 to 63', () => { + const v62Stub = { ...v62Schema } + const v63 = migrations[63](v62Stub) + + expect(v63.wallet.isUnlocked).toBe(undefined) + }) }) diff --git a/apps/mobile/src/app/migrations.ts b/apps/mobile/src/app/migrations.ts index 266352da374..f1041c618f4 100644 --- a/apps/mobile/src/app/migrations.ts +++ b/apps/mobile/src/app/migrations.ts @@ -875,4 +875,11 @@ export const migrations = { return newState }, + + 63: function removeWalletIsUnlockedState(state: any) { + const newState = { ...state } + delete newState.wallet.isUnlocked + + return newState + }, } diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx index c378c139fd7..6c076233cb6 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -22,8 +22,6 @@ import { useSporeColors, } from 'ui/src' import { spacing } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' import { isAndroid } from 'uniswap/src/utils/platform' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { ActionSheetModal, MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal' @@ -76,7 +74,6 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme const dispatch = useAppDispatch() const hasImportedSeedPhrase = useNativeAccountExists() const modalState = useAppSelector(selectModalState(ModalName.AccountSwitcher)) - const unitagsFeatureFlagEnabled = useFeatureFlag(FeatureFlags.Unitags) const onCompleteOnboarding = useCompleteOnboardingCallback({ entryPoint: OnboardingEntryPoint.Sidebar, importType: hasImportedSeedPhrase ? ImportType.CreateAdditional : ImportType.CreateNew, @@ -132,25 +129,15 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme dispatch(pendingAccountActions.trigger(PendingAccountActions.ActivateOneAndDelete)) dispatch(createAccountActions.trigger()) - if (unitagsFeatureFlagEnabled) { - if (hasImportedSeedPhrase) { - setCreatedAdditionalAccount(true) - } else { - // create pending account and place into welcome flow - navigate(Screens.OnboardingStack, { - screen: OnboardingScreens.WelcomeWallet, - params: { - importType: ImportType.CreateNew, - entryPoint: OnboardingEntryPoint.Sidebar, - }, - }) - } + if (hasImportedSeedPhrase) { + setCreatedAdditionalAccount(true) } else { + // create pending account and place into welcome flow navigate(Screens.OnboardingStack, { - screen: OnboardingScreens.EditName, + screen: OnboardingScreens.WelcomeWallet, params: { + importType: ImportType.CreateNew, entryPoint: OnboardingEntryPoint.Sidebar, - importType: hasImportedSeedPhrase ? ImportType.CreateAdditional : ImportType.CreateNew, }, }) } @@ -267,7 +254,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme } return options - }, [activeAccountAddress, dispatch, hasImportedSeedPhrase, onClose, t, unitagsFeatureFlagEnabled]) + }, [activeAccountAddress, dispatch, hasImportedSeedPhrase, onClose, t]) const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress) diff --git a/apps/mobile/src/app/modals/ExperimentsModal.tsx b/apps/mobile/src/app/modals/ExperimentsModal.tsx index 36acfbf902c..c6b2f3c3341 100644 --- a/apps/mobile/src/app/modals/ExperimentsModal.tsx +++ b/apps/mobile/src/app/modals/ExperimentsModal.tsx @@ -6,11 +6,6 @@ import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { closeModal } from 'src/features/modals/modalSlice' import { selectCustomEndpoint } from 'src/features/tweaks/selectors' import { setCustomEndpoint } from 'src/features/tweaks/slice' -import { - ConfigResult, - Statsig, - useExperimentWithExposureLoggingDisabled, -} from 'statsig-react-native' import { Accordion, Button, @@ -23,15 +18,20 @@ import { } from 'ui/src' import { spacing } from 'ui/src/theme' import { - EXPERIMENT_VALUES_BY_EXPERIMENT, - ExperimentsWallet, -} from 'uniswap/src/features/experiments/constants' + Experiments, + WALLET_EXPERIMENTS, + getExperimentDefinition, +} from 'uniswap/src/features/gating/experiments' import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES, getFeatureFlagName, -} from 'uniswap/src/features/experiments/flags' -import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/experiments/hooks' +} from 'uniswap/src/features/gating/flags' +import { + useExperimentValueWithExposureLoggingDisabled, + useFeatureFlagWithExposureLoggingDisabled, +} from 'uniswap/src/features/gating/hooks' +import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { Switch } from 'wallet/src/components/buttons/Switch' import { TextInput } from 'wallet/src/components/input/TextInput' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' @@ -65,11 +65,16 @@ export function ExperimentsModal(): JSX.Element { } } - const featureFlagRows = [] + const featureFlagRows: JSX.Element[] = [] for (const [flag, flagName] of WALLET_FEATURE_FLAG_NAMES.entries()) { featureFlagRows.push() } + const experimentRows: JSX.Element[] = [] + for (const [experiment, experimentDef] of WALLET_EXPERIMENTS.entries()) { + experimentRows.push() + } + return ( - {Object.values(ExperimentsWallet).map((experiment) => { - return - })} + {experimentRows} @@ -196,75 +199,52 @@ function FeatureFlagRow({ flag }: { flag: FeatureFlags }): JSX.Element { ) } -function ExperimentRow({ name }: { name: string }): JSX.Element { - const experiment = useExperimentWithExposureLoggingDisabled(name) - - const params = Object.entries(experiment.config.value).map(([key, value]) => ( - - {key} - - - )) +function ExperimentRow({ experiment }: { experiment: Experiments }): JSX.Element { + const experimentDef = getExperimentDefinition(experiment) return ( <> - {name} - {params} + {experimentDef.name} + + + + + + ) } -function ExperimentValueSwitch({ - experiment, - configValueContent, - configValueName, -}: { - experiment: ConfigResult - configValueContent: unknown - configValueName: string -}): JSX.Element { +function ExperimentValueSwitch({ experiment }: { experiment: Experiments }): JSX.Element { const colors = useSporeColors() - const experimentName = experiment.config.getName() - - const onValueChange = (newValue: boolean | string): void => { - Statsig.overrideConfig(experimentName, { - ...experiment.config.value, - [configValueName]: newValue, - }) - } + const experimentDef = getExperimentDefinition(experiment) + const currentValue = useExperimentValueWithExposureLoggingDisabled(experiment) - if (typeof configValueContent === 'boolean') { - return - } - - const variants = EXPERIMENT_VALUES_BY_EXPERIMENT[experimentName]?.[configValueName] - - if (variants && typeof configValueContent === 'string') { - return ( - - {Object.entries(variants).map(([_, value]) => ( - onValueChange(value)}> - - {value} - - - ))} - - ) - } - - return Unknown Variants + return ( + + {experimentDef.values.map((value) => ( + { + Statsig.overrideConfig(experimentDef.name, { + [experimentDef.key]: value, + }) + }}> + + {value} + + + ))} + + ) } diff --git a/apps/mobile/src/app/modals/TransferTokenModal.tsx b/apps/mobile/src/app/modals/TransferTokenModal.tsx index 005feb4d92d..d58404231e6 100644 --- a/apps/mobile/src/app/modals/TransferTokenModal.tsx +++ b/apps/mobile/src/app/modals/TransferTokenModal.tsx @@ -5,8 +5,8 @@ import { selectModalState } from 'src/features/modals/selectModalState' import { TransferFlow } from 'src/features/transactions/transfer/TransferFlow' import { TransferFlow as TransferFlowRewrite } from 'src/features/transactions/transfer/transferRewrite/TransferFlow' import { useSporeColors } from 'ui/src' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { ModalName } from 'wallet/src/telemetry/constants' diff --git a/apps/mobile/src/app/navigation/hooks.ts b/apps/mobile/src/app/navigation/hooks.ts index ea5cb48c2c5..22d735e6ae8 100644 --- a/apps/mobile/src/app/navigation/hooks.ts +++ b/apps/mobile/src/app/navigation/hooks.ts @@ -93,7 +93,7 @@ export function useEagerExternalProfileRootNavigation(): { /** * Utility hook that checks if the caller is part of the navigation tree. * - * Inspired by how the navigation library checks if the the navigation object exists. + * Inspired by how the navigation library checks if the navigation object exists. * https://github.com/react-navigation/react-navigation/blob/d7032ba8bb6ae24030a47f0724b61b561132fca6/packages/core/src/useNavigation.tsx#L18 */ export function useIsPartOfNavigationTree(): boolean { diff --git a/apps/mobile/src/app/navigation/navigation.tsx b/apps/mobile/src/app/navigation/navigation.tsx index d26e7e73c3e..37ffe38769c 100644 --- a/apps/mobile/src/app/navigation/navigation.tsx +++ b/apps/mobile/src/app/navigation/navigation.tsx @@ -41,7 +41,6 @@ import { BackupScreen } from 'src/screens/Onboarding/BackupScreen' import { CloudBackupPasswordConfirmScreen } from 'src/screens/Onboarding/CloudBackupPasswordConfirmScreen' import { CloudBackupPasswordCreateScreen } from 'src/screens/Onboarding/CloudBackupPasswordCreateScreen' import { CloudBackupProcessingScreen } from 'src/screens/Onboarding/CloudBackupProcessingScreen' -import { EditNameScreen } from 'src/screens/Onboarding/EditNameScreen' import { LandingScreen } from 'src/screens/Onboarding/LandingScreen' import { ManualBackupScreen } from 'src/screens/Onboarding/ManualBackupScreen' import { NotificationsSetupScreen } from 'src/screens/Onboarding/NotificationsSetupScreen' @@ -64,8 +63,8 @@ import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen' import { WebViewScreen } from 'src/screens/WebViewScreen' import { Icons, useDeviceInsets, useSporeColors } from 'ui/src' import { spacing } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' @@ -249,7 +248,6 @@ export function OnboardingStackNavigator(): JSX.Element { name={UnitagScreens.ChooseProfilePicture} options={{ ...TransitionPresets.ModalFadeTransition }} /> - v0Schema -export const getSchema = (): typeof v62Schema => v62Schema +export const getSchema = (): typeof v63Schema => v63Schema diff --git a/apps/mobile/src/app/store.ts b/apps/mobile/src/app/store.ts index 537c0208fe8..12431a0cc90 100644 --- a/apps/mobile/src/app/store.ts +++ b/apps/mobile/src/app/store.ts @@ -10,7 +10,7 @@ import { logger } from 'utilities/src/logger/logger' import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api' import { importAccountSagaName } from 'wallet/src/features/wallet/import/importAccountSaga' import { createStore } from 'wallet/src/state' -import { RootReducerNames } from 'wallet/src/state/reducer' +import { RootReducerNames, sharedPersistedStateWhitelist } from 'wallet/src/state/reducer' import { MobileState, ReducerNames, mobileReducer } from './reducer' import { mobileSaga } from './saga' @@ -54,28 +54,19 @@ const rtkQueryErrorLogger: Middleware = () => (next) => (action: PayloadAction = [ - 'appearanceSettings', - 'behaviorHistory', + ...sharedPersistedStateWhitelist, 'biometricSettings', - 'favorites', - 'notifications', 'passwordLockout', - 'searchHistory', 'telemetry', - 'tokens', - 'transactions', 'tweaks', - 'wallet', 'cloudBackup', - 'languageSettings', - 'fiatCurrencySettings', ] export const persistConfig = { key: 'root', storage: reduxStorage, whitelist, - version: 62, + version: 63, migrate: createMigrate(migrations), } diff --git a/apps/mobile/src/components/NFT/NftView.tsx b/apps/mobile/src/components/NFT/NftView.tsx index 708db8fea88..8cade9cab67 100644 --- a/apps/mobile/src/components/NFT/NftView.tsx +++ b/apps/mobile/src/components/NFT/NftView.tsx @@ -1,5 +1,4 @@ import ContextMenu from 'react-native-context-menu-view' -import { useNFTMenu } from 'src/features/nfts/hooks' import { Flex, ImpactFeedbackStyle, TouchableArea } from 'ui/src' import { borderRadii } from 'ui/src/theme' import noop from 'utilities/src/react/noop' @@ -9,6 +8,7 @@ import { MAX_NFT_IMAGE_SIZE, } from 'wallet/src/features/nfts/constants' import { NFTItem } from 'wallet/src/features/nfts/types' +import { useNFTContextMenu } from 'wallet/src/features/nfts/useNftContextMenu' export function NftView({ owner, @@ -19,7 +19,7 @@ export function NftView({ item: NFTItem onPress: () => void }): JSX.Element { - const { menuActions, onContextMenuPress } = useNFTMenu({ + const { menuActions, onContextMenuPress } = useNFTContextMenu({ contractAddress: item.contractAddress, tokenId: item.tokenId, owner, diff --git a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx index 9c8e32dd696..80f2d3d056d 100644 --- a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx +++ b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx @@ -128,7 +128,9 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E }, [permissionStatus, requestPermissionResponse, t]) const overlayWidth = (overlayLayout?.height ?? 0) / CAMERA_ASPECT_RATIO - const scannerSize = Math.min(overlayWidth, dimensions.fullWidth) * SCAN_ICON_WIDTH_RATIO + const cameraWidth = dimensions.fullWidth + const cameraHeight = CAMERA_ASPECT_RATIO * cameraWidth + const scannerSize = Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO return ( - + {permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && ( void onConfirm: () => void + disableConfirm?: boolean } > @@ -46,6 +47,7 @@ export function ModalWithOverlay({ scrollDownButtonText, onReject, onConfirm, + disableConfirm, ...bottomSheetModalProps }: ModalWithOverlayProps): JSX.Element { const scrollViewRef = useRef(null) @@ -124,7 +126,7 @@ export function ModalWithOverlay({ { + const getReadableMethodName = ( + ethMethod: EthMethod | UwULinkMethod, + dappNameOrUrl: string + ): JSX.Element => { switch (ethMethod) { case EthMethod.PersonalSign: case EthMethod.EthSign: case EthMethod.SignTypedData: return case EthMethod.EthSendTransaction: + case UwULinkMethod.Erc20Send: return } diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/UwULinkErc20SendModal.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/UwULinkErc20SendModal.tsx new file mode 100644 index 00000000000..3b82d743ead --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/RequestModal/UwULinkErc20SendModal.tsx @@ -0,0 +1,137 @@ +import { useBottomSheetInternal } from '@gorhom/bottom-sheet' +import { formatUnits } from 'ethers/lib/utils' +import { useTranslation } from 'react-i18next' +import Animated, { useAnimatedStyle } from 'react-native-reanimated' +import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay' +import { UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice' +import { Flex, Text } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' +import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' +import { CHAIN_INFO } from 'wallet/src/constants/chains' +import { useOnChainCurrencyBalance } from 'wallet/src/features/portfolio/api' +import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' + +type Props = { + onClose: () => void + onConfirm: () => void + onReject: () => void + request: UwuLinkErc20Request + hasSufficientGasFunds: boolean +} + +export function UwULinkErc20SendModal({ + onClose, + onConfirm, + onReject, + request, + hasSufficientGasFunds, +}: Props): JSX.Element { + const { t } = useTranslation() + const activeAccountAddress = useActiveAccountAddressWithThrow() + // TODO: wallet should determine if the currency is stablecoin + const { chainId, tokenAddress, amount } = request + const currencyInfo = useCurrencyInfo(buildCurrencyId(chainId, tokenAddress)) + const { balance } = useOnChainCurrencyBalance(currencyInfo?.currency, activeAccountAddress) + + const hasSufficientTokenFunds = !balance?.lessThan(amount) + + return ( + + + + ) +} + +function UwULinkErc20SendModalContent({ + request, + loading, + currencyInfo, + hasSufficientGasFunds, + hasSufficientTokenFunds, +}: { + request: UwuLinkErc20Request + loading: boolean + hasSufficientGasFunds: boolean + hasSufficientTokenFunds: boolean + currencyInfo: Maybe +}): JSX.Element { + const { t } = useTranslation() + const { animatedFooterHeight } = useBottomSheetInternal() + const bottomSpacerStyle = useAnimatedStyle(() => ({ + height: animatedFooterHeight.value, + })) + + const { chainId, isStablecoin } = request + const nativeCurrency = chainId && NativeCurrency.onChain(chainId) + + if (loading || !currencyInfo) { + return ( + + + + + ) + } + + const { + logoUrl, + currency: { name, symbol, decimals }, + } = currencyInfo + + return ( + + {request.recipient.name} + + {!hasSufficientTokenFunds && ( + + {t('uwulink.error.insufficientTokens', { + tokenSymbol: symbol, + chain: CHAIN_INFO[chainId].label, + })} + + )} + {`${isStablecoin ? '$' : ''}${formatUnits( + request.amount, + decimals + )}`} + + + {symbol} + + + {!hasSufficientGasFunds && ( + + {t('walletConnect.request.error.insufficientFunds', { + currencySymbol: nativeCurrency?.symbol, + })} + + )} + + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx index e02a892dd53..06eba03c531 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx @@ -18,21 +18,26 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga' import { - SignRequest, - TransactionRequest, + WalletConnectRequest, isTransactionRequest, } from 'src/features/walletConnect/walletConnectSlice' import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' import { GasSpeed } from 'wallet/src/features/gas/types' import { useIsBlocked, useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { EthMethod, WCEventType, WCRequestOutcome } from 'wallet/src/features/walletConnect/types' +import { + EthMethod, + UwULinkMethod, + WCEventType, + WCRequestOutcome, +} from 'wallet/src/features/walletConnect/types' import { ModalName } from 'wallet/src/telemetry/constants' import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { UwULinkErc20SendModal } from './UwULinkErc20SendModal' interface Props { onClose: () => void - request: SignRequest | TransactionRequest + request: WalletConnectRequest } const VALID_REQUEST_TYPES = [ @@ -41,6 +46,7 @@ const VALID_REQUEST_TYPES = [ EthMethod.SignTypedDataV4, EthMethod.EthSign, EthMethod.EthSendTransaction, + UwULinkMethod.Erc20Send, ] export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Element | null { @@ -62,6 +68,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem areAddressesEqual(account.address, request.account) ) const gasFee = useTransactionGasFee(tx, GasSpeed.Urgent) + const hasSufficientFunds = useHasSufficientFunds({ account: request.account, chainId, @@ -145,7 +152,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem if (!confirmEnabled || !signerAccount) { return } - if (request.type === EthMethod.EthSendTransaction) { + if (request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send) { if (!gasFee.params) { return } // appeasing typescript @@ -153,7 +160,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem signWcRequestActions.trigger({ sessionId: request.sessionId, requestInternalId: request.internalId, - method: request.type, + method: EthMethod.EthSendTransaction, transaction: { ...tx, ...gasFee.params }, account: signerAccount, dapp: request.dapp, @@ -198,6 +205,14 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem const { trigger: actionButtonTrigger } = useBiometricPrompt(onConfirm) const { requiredForTransactions } = useBiometricAppSettings() + const onConfirmPress = async (): Promise => { + if (requiredForTransactions) { + await actionButtonTrigger() + } else { + await onConfirm() + } + } + if (!VALID_REQUEST_TYPES.includes(request.type)) { return null } @@ -210,6 +225,18 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem } } + if (request.type === UwULinkMethod.Erc20Send) { + return ( + + ) + } + return ( => { - if (requiredForTransactions) { - await actionButtonTrigger() - } else { - await onConfirm() - } - }} + onConfirm={onConfirmPress} onReject={onReject}> return { currencyId, amount } } catch (error) { - logger.error(error, { tags: { file: 'WalletConnectRequestModal', function: 'getPermitInfo' } }) + logger.error(error, { + tags: { file: 'WalletConnectRequestModal', function: 'getPermitInfo' }, + }) return undefined } } @@ -60,7 +63,7 @@ const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined => type WalletConnectRequestModalContentProps = { gasFee: GasFeeResult hasSufficientFunds: boolean - request: WalletConnectRequest + request: SignRequest | TransactionRequest isBlocked: boolean } diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx index d7f74317db1..1a7acaa99ba 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx @@ -1,8 +1,8 @@ import React from 'react' import { Flex, Separator, Text, Unicon, UniconV2, useSporeColors } from 'ui/src' import Check from 'ui/src/assets/icons/check.svg' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' 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' diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx index e5c09c6401e..0d976a7c1bd 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' import 'react-native-reanimated' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useAppDispatch } from 'src/app/hooks' import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' import Trace from 'src/components/Trace/Trace' @@ -10,8 +10,10 @@ import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ import { URIType, UWULINK_PREFIX, + findAllowedTokenRecipient, getSupportedURI, isAllowedUwuLinkRequest, + toTokenTransferRequest, useUwuLinkContractAllowlist, } from 'src/components/WalletConnect/ScanSheet/util' import { BackButtonView } from 'src/components/layout/BackButtonView' @@ -23,14 +25,15 @@ import { Flex, HapticFeedback, Text, TouchableArea, useIsDarkMode, useSporeColor 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 { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { logger } from 'utilities/src/logger/logger' import { WalletQRCode } from 'wallet/src/components/QRCodeScanner/WalletQRCode' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' -import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' -import { EthMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types' +import { useContractManager, useProviderManager } from 'wallet/src/features/wallet/context' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' +import { EthMethod, UwULinkMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types' import { ElementName, ModalName } from 'wallet/src/telemetry/constants' type Props = { @@ -45,8 +48,8 @@ export function WalletConnectModal({ const { t } = useTranslation() const colors = useSporeColors() const isDarkMode = useIsDarkMode() - const activeAddress = useAppSelector(selectActiveAccountAddress) - const { sessions, hasPendingSessionError } = useWalletConnect(activeAddress) + const activeAccount = useActiveAccount() + const { sessions, hasPendingSessionError } = useWalletConnect(activeAccount?.address) const [currentScreenState, setCurrentScreenState] = useState(initialScreenState) const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false) @@ -57,6 +60,9 @@ export function WalletConnectModal({ const uwuLinkContractAllowlist = useUwuLinkContractAllowlist() + const providerManager = useProviderManager() + const contractManager = useContractManager() + // Update QR scanner states when pending session error alert is shown from WCv2 saga event channel useEffect(() => { if (hasPendingSessionError) { @@ -68,7 +74,7 @@ export function WalletConnectModal({ const onScanCode = useCallback( async (uri: string) => { // don't scan any QR codes if there is an error popup open or camera is frozen - if (!activeAddress || hasPendingSessionError || shouldFreezeCamera) { + if (!activeAccount || hasPendingSessionError || shouldFreezeCamera) { return } await HapticFeedback.selection() @@ -149,7 +155,6 @@ export function WalletConnectModal({ try { const parsedUwulinkRequest: UwULinkRequest = JSON.parse(supportedURI.value) const isAllowed = isAllowedUwuLinkRequest(parsedUwulinkRequest, uwuLinkContractAllowlist) - if (!isAllowed) { Alert.alert( t('walletConnect.error.uwu.title'), @@ -166,25 +171,62 @@ export function WalletConnectModal({ return } - dispatch( - addRequest({ - account: activeAddress, - request: { - type: EthMethod.EthSendTransaction, - transaction: { from: activeAddress, ...parsedUwulinkRequest.value }, - sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here - internalId: UWULINK_PREFIX, - account: activeAddress, - dapp: { - ...parsedUwulinkRequest.dapp, - source: UWULINK_PREFIX, - chain_id: parsedUwulinkRequest.chainId, - webhook: parsedUwulinkRequest.webhook, + const newRequest = { + sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here + internalId: UWULINK_PREFIX, + account: activeAccount?.address, + dapp: { + name: '', + url: '', + ...parsedUwulinkRequest.dapp, + source: UWULINK_PREFIX, + chain_id: parsedUwulinkRequest.chainId, + webhook: parsedUwulinkRequest.webhook, + }, + chainId: parsedUwulinkRequest.chainId, + } + + if (parsedUwulinkRequest.method === UwULinkMethod.Erc20Send) { + const preparedTransaction = await toTokenTransferRequest( + parsedUwulinkRequest, + activeAccount, + providerManager, + contractManager + ) + const tokenRecipient = findAllowedTokenRecipient( + parsedUwulinkRequest, + uwuLinkContractAllowlist + ) + + dispatch( + addRequest({ + account: activeAccount.address, + request: { + ...newRequest, + type: UwULinkMethod.Erc20Send, + recipient: { + address: parsedUwulinkRequest.recipient, + name: tokenRecipient?.name ?? '', + }, + amount: parsedUwulinkRequest.amount, + tokenAddress: parsedUwulinkRequest.tokenAddress, + isStablecoin: parsedUwulinkRequest.isStablecoin, + transaction: { from: activeAccount.address, ...preparedTransaction }, }, - chainId: parsedUwulinkRequest.chainId, - }, - }) - ) + }) + ) + } else { + dispatch( + addRequest({ + account: activeAccount.address, + request: { + ...newRequest, + type: EthMethod.EthSendTransaction, + transaction: { from: activeAccount.address, ...parsedUwulinkRequest.value }, + }, + }) + ) + } onClose() } catch (_) { setShouldFreezeCamera(false) @@ -206,7 +248,7 @@ export function WalletConnectModal({ } }, [ - activeAddress, + activeAccount, hasPendingSessionError, shouldFreezeCamera, isUwULinkEnabled, @@ -217,6 +259,8 @@ export function WalletConnectModal({ onClose, dispatch, uwuLinkContractAllowlist, + providerManager, + contractManager, ] ) @@ -236,7 +280,7 @@ export function WalletConnectModal({ setCurrentScreenState(ScannerModalState.ScanQr) } - if (!activeAddress) { + if (!activeAccount) { return null } @@ -269,7 +313,7 @@ export function WalletConnectModal({ )} {currentScreenState === ScannerModalState.WalletQr && ( - + )} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts b/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts index 40e0bdc703e..aa264cc20a6 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts @@ -6,12 +6,24 @@ import { UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, UNISWAP_WALLETCONNECT_URL, } from 'src/features/deepLinking/constants' -import { DynamicConfigs } from 'uniswap/src/features/experiments/configs' -import { useDynamicConfig } from 'uniswap/src/features/experiments/hooks' +import { DynamicConfigs } from 'uniswap/src/features/gating/configs' +import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' import { logger } from 'utilities/src/logger/logger' +import { RPCType } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { ContractManager } from 'wallet/src/features/contracts/ContractManager' +import { ProviderManager } from 'wallet/src/features/providers' import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types' -import { UwULinkRequest } from 'wallet/src/features/walletConnect/types' -import { getValidAddress } from 'wallet/src/utils/addresses' +import { getTokenTransferRequest } from 'wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest' +import { TransferCurrencyParams } from 'wallet/src/features/transactions/transfer/types' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { + EthTransaction, + UwULinkErc20SendRequest, + UwULinkMethod, + UwULinkRequest, +} from 'wallet/src/features/walletConnect/types' +import { areAddressesEqual, getValidAddress } from 'wallet/src/utils/addresses' export enum URIType { WalletConnectURL = 'walletconnect', @@ -34,19 +46,23 @@ interface EnabledFeatureFlags { // This type must match the format in statsig dynamic config for uwulink // https://console.statsig.com/5HjUux4OvSGzgqWIfKFt8i/dynamic_configs/uwulink_config -type UwuLinkAllowlistItem = { +type UwULinkAllowlistItem = { chainId: number - contractAddress: string + address: string name: string icon?: string } -type UwuLinkAllowlist = UwuLinkAllowlistItem[] + +type UwULinkAllowlist = { + contracts: UwULinkAllowlistItem[] + tokenRecipients: UwULinkAllowlistItem[] +} const UWULINK_MAX_TXN_VALUE = '0.001' const EASTER_EGG_QR_CODE = 'DO_NOT_SCAN_OR_ELSE_YOU_WILL_GO_TO_MOBILE_TEAM_JAIL' export const CUSTOM_UNI_QR_CODE_PREFIX = 'hello_uniwallet:' -export const UWULINK_PREFIX = 'uwulink' +export const UWULINK_PREFIX = 'uwulink' as const export const truncateQueryParams = (url: string): string => { // In fact, the first element will be always returned below. url is @@ -102,8 +118,9 @@ export async function getSupportedURI( return { type: URIType.EasterEgg, value: uri } } - if (enabledFeatureFlags?.isUwULinkEnabled && isUwULink(uri)) { - return { type: URIType.UwULink, value: uri.slice(UWULINK_PREFIX.length) } + if (isUwULink(uri)) { + // remove escape strings from the stringified JSON before parsing it + return { type: URIType.UwULink, value: uri.slice(UWULINK_PREFIX.length).replaceAll('\\', '') } } } @@ -132,11 +149,24 @@ function isUwULink(uri: string): boolean { // Gets the UWULink contract allow list from statsig dynamic config. // We can safely cast as long as the statsig config format matches our `UwuLinkAllowlist` type. -export function useUwuLinkContractAllowlist(): UwuLinkAllowlist { +export function useUwuLinkContractAllowlist(): UwULinkAllowlist { const uwuLinkConfig = useDynamicConfig(DynamicConfigs.UwuLink) - return uwuLinkConfig.getValue('allowlist') as UwuLinkAllowlist + return uwuLinkConfig.getValue('allowlist') as UwULinkAllowlist } +export function findAllowedTokenRecipient( + request: UwULinkRequest, + allowlist: UwULinkAllowlist +): UwULinkAllowlistItem | undefined { + if (request.method !== UwULinkMethod.Erc20Send) { + return + } + + const { chainId, recipient } = request + return allowlist.tokenRecipients.find( + (item) => item.chainId === chainId && areAddressesEqual(item.address, recipient) + ) +} /** * Util function to check if a UwULinkRequest is valid. * @@ -144,17 +174,26 @@ export function useUwuLinkContractAllowlist(): UwuLinkAllowlist { * 1. The to address is in the UWULINK_CONTRACT_ALLOWLIST * 2. The value is less than or equal to UWULINK_MAX_TXN_VALUE * + * TODO: also check for validity of the entire request object (e.g. all the required fields exist) + * * @param request parsed UwULinkRequest * @returns boolean for whether the UwULinkRequest is allowed */ export function isAllowedUwuLinkRequest( request: UwULinkRequest, - allowList: UwuLinkAllowlist + allowlist: UwULinkAllowlist ): boolean { + // token sends + if (request.method === UwULinkMethod.Erc20Send) { + return Boolean(findAllowedTokenRecipient(request, allowlist)) + } + + // generic transactions const { to, value } = request.value const belowMaximumValue = value && parseFloat(value) <= parseEther(UWULINK_MAX_TXN_VALUE).toNumber() - const isAllowedContractAddress = to && allowList.some((item) => item.contractAddress === to) + const isAllowedContractAddress = + to && allowlist.contracts.some((item) => areAddressesEqual(item.address, to)) if (!belowMaximumValue || !isAllowedContractAddress) { return false @@ -238,3 +277,22 @@ export function parseScantasticParams(uri: string): ScantasticParams | undefined }) } } + +export async function toTokenTransferRequest( + request: UwULinkErc20SendRequest, + account: Account, + providerManager: ProviderManager, + contractManager: ContractManager +): Promise { + const provider = providerManager.getProvider(request.chainId, RPCType.Public) + const params: TransferCurrencyParams = { + type: AssetType.Currency, + account, + chainId: request.chainId, + toAddress: request.recipient, + tokenAddress: request.tokenAddress, + amountInWei: request.amount.toString(), + } + const transaction = await getTokenTransferRequest(params, provider, contractManager) + return transaction as EthTransaction +} diff --git a/apps/mobile/src/components/accounts/AccountHeader.tsx b/apps/mobile/src/components/accounts/AccountHeader.tsx index 374a9e1af5e..9f7bc14597d 100644 --- a/apps/mobile/src/components/accounts/AccountHeader.tsx +++ b/apps/mobile/src/components/accounts/AccountHeader.tsx @@ -5,8 +5,8 @@ import { openModal } from 'src/features/modals/modalSlice' import { setUserProperty } from 'src/features/telemetry' import { UserPropertyName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' -import { isDevBuild } from 'src/utils/version' import { Flex, HapticFeedback, Icons, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' +import { isDevEnv } from 'uniswap/src/utils/env' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName' import { pushNotification } from 'wallet/src/features/notifications/slice' @@ -87,7 +87,7 @@ export function AccountHeader(): JSX.Element { hitSlop={20} testID={ElementName.Manage} onLongPress={async (): Promise => { - if (isDevBuild()) { + if (isDevEnv()) { await HapticFeedback.selection() dispatch(openModal({ name: ModalName.Experiments })) } diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx index 4b1b14a3aa0..d4fc81dd524 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx @@ -3,8 +3,8 @@ import configureMockStore from 'redux-mock-store' import { Screens } from 'src/screens/Screens' import { preloadedMobileState } from 'src/test/fixtures' import { fireEvent, render } from 'src/test/test-utils' +import * as unitagHooks from 'uniswap/src/features/unitags/hooks' import * as ensHooks from 'wallet/src/features/ens/api' -import * as unitagHooks from 'wallet/src/features/unitags/hooks' import { ON_PRESS_EVENT_PAYLOAD, SAMPLE_SEED_ADDRESS_1, diff --git a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx index 75fddeefb0d..2bb76450a41 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' @@ -10,14 +10,8 @@ import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHe import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import ClockIcon from 'ui/src/assets/icons/clock.svg' import { iconSizes } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' import { clearSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { - SearchResult, - SearchResultType, - WalletSearchResult, -} from 'wallet/src/features/search/SearchResult' +import { SearchResultType, WalletSearchResult } from 'wallet/src/features/search/SearchResult' import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory' const TrendUpIcon = @@ -39,28 +33,11 @@ export function SearchEmptySection(): JSX.Element { const { t } = useTranslation() const dispatch = useAppDispatch() const searchHistory = useAppSelector(selectSearchHistory) - const unitagFeatureFlagEnabled = useFeatureFlag(FeatureFlags.Unitags) const onPressClearSearchHistory = (): void => { dispatch(clearSearchHistory()) } - const modifiedHistory: SearchResult[] = useMemo( - () => - searchHistory.map((historyItem: SearchResult) => { - if (!unitagFeatureFlagEnabled && historyItem.type === SearchResultType.Unitag) { - return { - type: SearchResultType.WalletByAddress, - address: historyItem.address, - searchId: historyItem.searchId, - } - } else { - return historyItem - } - }), - [searchHistory, unitagFeatureFlagEnabled] - ) - // Show search history (if applicable), trending tokens, and wallets return ( @@ -85,7 +62,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/hooks.ts b/apps/mobile/src/components/explore/search/hooks.ts index 2c2e4f5b508..50739da8a77 100644 --- a/apps/mobile/src/components/explore/search/hooks.ts +++ b/apps/mobile/src/components/explore/search/hooks.ts @@ -1,9 +1,9 @@ import { useMemo } from 'react' +import { useUnitagByAddress, useUnitagByName } from 'uniswap/src/features/unitags/hooks' import { ChainId } from 'wallet/src/constants/chains' import { useENS } from 'wallet/src/features/ens/useENS' import { SearchResultType, 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' import { getValidAddress } from 'wallet/src/utils/addresses' // eslint-disable-next-line complexity diff --git a/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx b/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx index 657cec4e7df..8853f43a17f 100644 --- a/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx +++ b/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx @@ -6,7 +6,7 @@ import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { concatStrings } from 'utilities/src/primitives/string' import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' -import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { getOptionalServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' import { ImageUri } from 'wallet/src/features/images/ImageUri' function LogoLoader(): JSX.Element { @@ -24,7 +24,7 @@ export function FORQuoteItem({ }): JSX.Element | null { const { t } = useTranslation() const isDarkMode = useIsDarkMode() - const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode) + const logoUrl = getOptionalServiceProviderLogo(serviceProvider?.logos, isDarkMode) if (!serviceProvider) { return null diff --git a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx index cf2ea7bf6a0..dcd375b6b84 100644 --- a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx +++ b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx @@ -5,8 +5,8 @@ import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay' import { APP_STORE_LINK } from 'src/constants/urls' import { UpgradeStatus } from 'src/features/forceUpgrade/types' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' -import { DynamicConfigs } from 'uniswap/src/features/experiments/configs' -import { useDynamicConfig } from 'uniswap/src/features/experiments/hooks' +import { DynamicConfigs } from 'uniswap/src/features/gating/configs' +import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' diff --git a/apps/mobile/src/components/home/WalletEmptyState.tsx b/apps/mobile/src/components/home/WalletEmptyState.tsx index b2fae660b77..cd94b4c5453 100644 --- a/apps/mobile/src/components/home/WalletEmptyState.tsx +++ b/apps/mobile/src/components/home/WalletEmptyState.tsx @@ -1,23 +1,34 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { + ImageBackground, + ImageSourcePropType, + StyleProp, + StyleSheet, + ViewStyle, + VirtualizedList, +} from 'react-native' import { useAppDispatch } from 'src/app/hooks' import Trace from 'src/components/Trace/Trace' import { openModal } from 'src/features/modals/modalSlice' -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 { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { CRYPTO_PURCHASE_BACKGROUND_DARK, CRYPTO_PURCHASE_BACKGROUND_LIGHT } from 'ui/src/assets' +import { ArrowDownCircle, Buy as BuyIcon, PaperStack } from 'ui/src/components/icons' +import { borderRadii } from 'ui/src/theme' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' +import { useCexTransferProviders } from 'wallet/src/features/fiatOnRamp/api' +import { ImageUri } from 'wallet/src/features/images/ImageUri' 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 { title: string blurb: string icon: JSX.Element + backgroundImage?: ImageSourcePropType onPress: () => void elementName: ElementNameType badgeText?: string @@ -29,13 +40,19 @@ enum ActionOption { Receive = 'Receive', } +const ICON_SIZE = 28 +const ICON_SHIFT = -10 + export function WalletEmptyState(): JSX.Element { const { t } = useTranslation() const dispatch = useAppDispatch() + const isDarkMode = useIsDarkMode() const activeAccount = useActiveAccount() const isViewOnly = activeAccount?.type === AccountType.Readonly const forAggregatorEnabled = useFeatureFlag(FeatureFlags.ForAggregator) + const cexTransferEnabled = useFeatureFlag(FeatureFlags.CexTransfers) + const cexTransferProviders = useCexTransferProviders(cexTransferEnabled) const options: { [key in ActionOption]: ActionCardItem } = useMemo( () => ({ @@ -43,17 +60,10 @@ export function WalletEmptyState(): JSX.Element { title: t('home.tokens.empty.action.buy.title'), blurb: t('home.tokens.empty.action.buy.description'), elementName: ElementName.EmptyStateBuy, - icon: ( - } - /> - } - /> - ), + icon: , + backgroundImage: isDarkMode + ? CRYPTO_PURCHASE_BACKGROUND_DARK + : CRYPTO_PURCHASE_BACKGROUND_LIGHT, onPress: () => dispatch( openModal({ @@ -65,45 +75,38 @@ export function WalletEmptyState(): JSX.Element { title: t('home.tokens.empty.action.receive.title'), blurb: t('home.tokens.empty.action.receive.description'), elementName: ElementName.EmptyStateReceive, - icon: ( - } - /> - } - /> - ), + icon: + cexTransferProviders.length > 0 ? ( + provider.logos.lightLogo)} + /> + ) : ( + + ), onPress: () => dispatch( - openModal({ - name: ModalName.WalletConnectScan, - initialState: ScannerModalState.WalletQr, - }) + openModal( + cexTransferProviders.length > 0 + ? { + name: ModalName.ReceiveCryptoModal, + initialState: cexTransferProviders, + } + : { + name: ModalName.WalletConnectScan, + initialState: ScannerModalState.WalletQr, + } + ) ), }, [ActionOption.Import]: { title: t('home.tokens.empty.action.import.title'), blurb: t('home.tokens.empty.action.import.description'), elementName: ElementName.EmptyStateImport, - icon: ( - - } - /> - ), + icon: , onPress: () => dispatch(openModal({ name: ModalName.AccountSwitcher })), }, }), - [dispatch, forAggregatorEnabled, t] + [t, isDarkMode, cexTransferProviders, dispatch, forAggregatorEnabled] ) // Order options based on view only status @@ -118,37 +121,167 @@ export function WalletEmptyState(): JSX.Element { ) } -const ActionCard = ({ title, blurb, onPress, icon, elementName }: ActionCardItem): JSX.Element => ( +const ActionCard = ({ + title, + blurb, + onPress, + icon, + elementName, + backgroundImage, +}: ActionCardItem): JSX.Element => ( - - - {icon} - - - {title} + + + + {icon} + + + {title} + + + {blurb} + - - {blurb} - - + ) -const IconContainer = ({ - backgroundColor, - icon, +const BackgroundWrapper = ({ + children, + backgroundImage, }: { - backgroundColor: string - icon: JSX.Element -}): JSX.Element => ( - - {icon} - -) + children: React.ReactNode + backgroundImage?: ImageSourcePropType +}): JSX.Element => { + return backgroundImage !== undefined ? ( + + {children} + + ) : ( + {children} + ) +} + +function ReceiveCryptoIcon(): JSX.Element { + return ( + + + + ) +} + +function ServiceProviderLogo({ uri }: { uri: string }): JSX.Element { + return ( + + + + ) +} + +function renderItem({ item }: { item: string }): JSX.Element { + return item === 'icon' ? : +} + +function keyExtractor(item: string): string { + return item +} + +/* + * Set the zIndex to -index to reverse the order of the elements. + */ +const LogoRendererComponent = ({ + children, + index, + style, + ...props +}: { + children: React.ReactNode + index: number + style: StyleProp +}): JSX.Element => { + const cellStyle = [style, { zIndex: -index }] + + return ( + + {children} + + ) +} + +function OverlappingLogos({ logos }: { logos: string[] }): JSX.Element { + const getItem = (_data: unknown, index: number): string => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return index === 0 ? 'icon' : logos[index - 1]! + } + + const getItemCount = (): number => { + return logos.length + 1 + } + + return ( + + + horizontal + CellRendererComponent={LogoRendererComponent} + contentContainerStyle={{ + paddingRight: -ICON_SHIFT, + }} + getItem={getItem} + getItemCount={getItemCount} + keyExtractor={keyExtractor} + renderItem={renderItem} + /> + + ) +} + +const styles = StyleSheet.create({ + iconContainer: { + borderRadius: borderRadii.rounded8, + height: ICON_SIZE, + marginRight: ICON_SHIFT, + overflow: 'hidden', + width: ICON_SIZE, + }, +}) diff --git a/apps/mobile/src/data/usePersistedApolloClient.tsx b/apps/mobile/src/data/usePersistedApolloClient.tsx index e04b59f1dab..ef56a0ecb02 100644 --- a/apps/mobile/src/data/usePersistedApolloClient.tsx +++ b/apps/mobile/src/data/usePersistedApolloClient.tsx @@ -7,9 +7,6 @@ import { initAndPersistCache } from 'src/data/cache' import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { MobileEventName } from 'src/features/telemetry/constants' import { selectCustomEndpoint } from 'src/features/tweaks/selectors' -import { uniswapUrls } from 'uniswap/src/constants/urls' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' import { isNonJestDev } from 'utilities/src/environment' import { logger } from 'utilities/src/logger/logger' import { useAsyncData } from 'utilities/src/react/hooks' @@ -72,7 +69,6 @@ if (isNonJestDev) { export const usePersistedApolloClient = (): ApolloClient | undefined => { const [client, setClient] = useState>() const customEndpoint = useAppSelector(selectCustomEndpoint) - const cloudflareGatewayEnabled = useFeatureFlag(FeatureFlags.GatewayDNSUpdateMobile) const apolloLink = customEndpoint ? getCustomGraphqlHttpLink(customEndpoint) @@ -90,9 +86,7 @@ export const usePersistedApolloClient = (): ApolloClient ) } - const restLink = cloudflareGatewayEnabled - ? getRestLink(uniswapUrls.apiBaseUrlCloudflare) - : getRestLink() + const restLink = getRestLink() const newClient = new ApolloClient({ assumeImmutableResults: true, diff --git a/apps/mobile/src/features/analytics/appsflyer.tsx b/apps/mobile/src/features/analytics/appsflyer.tsx index b3dc3182051..9aaefd08136 100644 --- a/apps/mobile/src/features/analytics/appsflyer.tsx +++ b/apps/mobile/src/features/analytics/appsflyer.tsx @@ -1,13 +1,13 @@ import appsFlyer from 'react-native-appsflyer' -import { isBetaBuild, isDevBuild } from 'src/utils/version' import { config } from 'uniswap/src/config' +import { isBetaEnv, isDevEnv } from 'uniswap/src/utils/env' import { logger } from 'utilities/src/logger/logger' export function initAppsFlyer(): void { appsFlyer.initSdk( { devKey: config.appsflyerApiKey, - isDebug: isDevBuild() || isBetaBuild(), + isDebug: isDevEnv() || isBetaEnv(), appId: config.appsflyerAppId, onInstallConversionDataListener: false, onDeepLinkListener: false, diff --git a/apps/mobile/src/features/deepLinking/README.md b/apps/mobile/src/features/deepLinking/README.md index c6948b86d25..700c96361c3 100644 --- a/apps/mobile/src/features/deepLinking/README.md +++ b/apps/mobile/src/features/deepLinking/README.md @@ -1,6 +1,6 @@ # Universal Links -Universal links allow 3rd parties to prompt the app to open to specific screens when it is installed on their device. If the app isn't installed it will open that page in Safari (a 404 on uniswap.org in this case). All universal links must use the the prefix `https://uniswap.org/app`. +Universal links allow 3rd parties to prompt the app to open to specific screens when it is installed on their device. If the app isn't installed it will open that page in Safari (a 404 on uniswap.org in this case). All universal links must use the prefix `https://uniswap.org/app`. ## Supported Screens diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts index 64905e2f746..9dedc76a561 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts @@ -15,7 +15,7 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { MobileEventName } from 'src/features/telemetry/constants' import { waitForWcWeb3WalletIsReady } from 'src/features/walletConnect/saga' import { Screens } from 'src/screens/Screens' -import { UNISWAP_APP_HOSTNAME } from 'uniswap/src/constants/urls' +import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' import { setAccountAsActive } from 'wallet/src/features/wallet/slice' import { ModalName } from 'wallet/src/telemetry/constants' import { @@ -164,8 +164,8 @@ describe(handleDeepLink, () => { it('Handles Share NFT Item Universal Link', async () => { const path = `nfts/asset/${SAMPLE_SEED_ADDRESS_1}/123` - const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` - const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const pathUrl = `${UNISWAP_WEB_URL}/${path}` + const hashedUrl = `${UNISWAP_WEB_URL}/#/${path}` const expectedModal: OpenModalParams = { name: ModalName.Explore, initialState: { @@ -207,8 +207,8 @@ describe(handleDeepLink, () => { it('Handles Share NFT Collection Universal Link', async () => { const path = `nfts/collection/${SAMPLE_SEED_ADDRESS_1}` - const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` - const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const pathUrl = `${UNISWAP_WEB_URL}/${path}` + const hashedUrl = `${UNISWAP_WEB_URL}/#/${path}` const expectedModal: OpenModalParams = { name: ModalName.Explore, initialState: { @@ -248,8 +248,8 @@ describe(handleDeepLink, () => { it('Handles Share Token Item Universal Link', async () => { const path = `tokens/ethereum/${SAMPLE_SEED_ADDRESS_1}` - const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` - const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const pathUrl = `${UNISWAP_WEB_URL}/${path}` + const hashedUrl = `${UNISWAP_WEB_URL}/#/${path}` const expectedModal: OpenModalParams = { name: ModalName.Explore, initialState: { @@ -289,7 +289,7 @@ describe(handleDeepLink, () => { it('Handles Share currently active Account Address Universal Link', () => { const hash = `#/address/${account.address}` - const url = `https://${UNISWAP_APP_HOSTNAME}/${hash}` + const url = `${UNISWAP_WEB_URL}/${hash}` return expectSaga(handleDeepLink, { payload: { url, @@ -305,7 +305,7 @@ describe(handleDeepLink, () => { it('Handles Share already added Account Address Universal Link', () => { const hash = `#/address/${SAMPLE_SEED_ADDRESS_2}` - const url = `https://${UNISWAP_APP_HOSTNAME}/${hash}` + const url = `${UNISWAP_WEB_URL}/${hash}` return expectSaga(handleDeepLink, { payload: { url, @@ -330,8 +330,8 @@ describe(handleDeepLink, () => { it('Handles Share external Account Address Universal Link', async () => { const path = `address/${SAMPLE_SEED_ADDRESS_2}` - const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` - const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const pathUrl = `${UNISWAP_WEB_URL}/${path}` + const hashedUrl = `${UNISWAP_WEB_URL}/#/${path}` const expectedModal: OpenModalParams = { name: ModalName.Explore, initialState: { diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts index 57112eccf54..97bac994385 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts @@ -26,8 +26,8 @@ import { WidgetType } from 'src/features/widgets/widgets' import { Screens } from 'src/screens/Screens' import { Statsig } from 'statsig-react-native' import { call, put, takeLatest } from 'typed-redux-saga' -import { UNISWAP_APP_HOSTNAME } from 'uniswap/src/constants/urls' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/experiments/flags' +import { UNISWAP_WEB_HOSTNAME } from 'uniswap/src/constants/urls' +import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import i18n from 'uniswap/src/i18n/i18n' import { logger } from 'utilities/src/logger/logger' import { selectExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/selectors' @@ -217,7 +217,7 @@ export function* handleDeepLink(action: ReturnType) { if (!activeAccount) { // For app.uniswap.org links it should open a browser with the link // instead of handling it inside the app - if (url.hostname === UNISWAP_APP_HOSTNAME) { + if (url.hostname === UNISWAP_WEB_HOSTNAME) { yield* call(openUri, action.payload.url, /* openExternalBrowser */ true) } // Skip handling any other deep links @@ -315,8 +315,8 @@ export function* handleDeepLink(action: ReturnType) { } } - if (url.hostname === UNISWAP_APP_HOSTNAME) { - const urlParts = url.href.split(`${UNISWAP_APP_HOSTNAME}/`) + if (url.hostname === UNISWAP_WEB_HOSTNAME) { + const urlParts = url.href.split(`${UNISWAP_WEB_HOSTNAME}/`) const urlPath = urlParts.length >= 1 ? (urlParts[1] as string) : '' yield* call(handleUniswapAppDeepLink, urlPath, action.payload.url, LinkSource.Share) return diff --git a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx index 1e0099015d4..2c6b0b75a47 100644 --- a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx @@ -8,11 +8,11 @@ import { disableOnPress } from 'src/utils/disableOnPress' import { Flex, HapticFeedback, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { logger } from 'utilities/src/logger/logger' import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' -import { useUnitagByAddress } from 'wallet/src/features/unitags/hooks' import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' import { ShareableEntity, WalletEventName } from 'wallet/src/telemetry/constants' import { setClipboard } from 'wallet/src/utils/clipboard' diff --git a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx index 4cbacd1b2e6..287c2e7dd4d 100644 --- a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx @@ -27,8 +27,8 @@ import { } from 'ui/src' import { ENS_LOGO } from 'ui/src/assets' import { iconSizes, imageSizes } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { useENSDescription, useENSName, useENSTwitterUsername } from 'wallet/src/features/ens/api' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' diff --git a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModalState.ts b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModalState.ts index 4f2c949e03e..bc87e587550 100644 --- a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModalState.ts +++ b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModalState.ts @@ -1,5 +1,5 @@ -import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' +import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' export interface ExchangeTransferModalState { - serviceProvider: FORTransferInstitution + serviceProvider: FORServiceProvider } diff --git a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector.tsx b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector.tsx new file mode 100644 index 00000000000..751a1bdd00d --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector.tsx @@ -0,0 +1,110 @@ +import React, { useCallback } from 'react' +import { FlatList, ListRenderItemInfo } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { openModal } from 'src/features/modals/modalSlice' +import { AnimatedFlex, Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' +import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { RemoteImage } from 'wallet/src/features/images/RemoteImage' +import { ModalName } from 'wallet/src/telemetry/constants' + +function key(item: FORServiceProvider): string { + return item.serviceProvider +} + +const CEX_ICON_SIZE = iconSizes.icon36 +const CEX_ICON_BORDER_RADIUS = 12 + +function CEXItemWrapper({ + serviceProvider, + onSelectServiceProvider, +}: { + serviceProvider: FORServiceProvider + onSelectServiceProvider: (serviceProvider: FORServiceProvider) => void +}): JSX.Element | null { + const onPress = (): void => onSelectServiceProvider(serviceProvider) + + const isDarkMode = useIsDarkMode() + const logoUrl = getServiceProviderLogo(serviceProvider.logos, isDarkMode) + + return ( + + + + + + {serviceProvider.name} + + + + + ) +} + +export function ServiceProviderSelector({ + onClose, + serviceProviders, +}: { + onClose: () => void + serviceProviders: FORServiceProvider[] +}): JSX.Element { + const dispatch = useAppDispatch() + + const onSelectServiceProvider = useCallback( + (serviceProvider: FORServiceProvider) => { + dispatch( + openModal({ + name: ModalName.ExchangeTransferModal, + initialState: { serviceProvider }, + }) + ) + onClose() + }, + [dispatch, onClose] + ) + + const renderItem = useCallback( + ({ item: serviceProvider }: ListRenderItemInfo) => ( + + ), + [onSelectServiceProvider] + ) + + return ( + + + + + + ) +} + +const renderItemSeparator = (): JSX.Element => diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx index ea4a1ae3281..3b0d7c04c0a 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx @@ -48,6 +48,16 @@ const PREDEFINED_AMOUNTS = [100, 300, 1000] type OnChangeAmount = (amount: string) => void +function OnRampError({ errorText, color }: { errorText: string; color: ColorTokens }): JSX.Element { + return ( + + + {errorText} + + + ) +} + interface Props { showNativeKeyboard: boolean onInputPanelLayout: (event: LayoutChangeEvent) => void @@ -67,6 +77,7 @@ interface Props { onTokenSelectorPress: () => void predefinedAmountsSupported: boolean appFiatCurrencySupported: boolean + notAvailableInThisRegion?: boolean fiatCurrencyInfo: FiatCurrencyInfo } @@ -89,6 +100,7 @@ export function FiatOnRampAmountSection({ onTokenSelectorPress, predefinedAmountsSupported, appFiatCurrencySupported, + notAvailableInThisRegion, fiatCurrencyInfo, }: Props): JSX.Element { const { t } = useTranslation() @@ -186,7 +198,8 @@ export function FiatOnRampAmountSection({ {currency.currencyInfo && ( ))} ) : null} - {!appFiatCurrencySupported ? ( - - - {t('fiatOnRamp.error.usd')} - - + {notAvailableInThisRegion ? ( + + ) : !appFiatCurrencySupported ? ( + ) : null} @@ -221,6 +233,7 @@ interface SelectTokenButtonProps { onPress: () => void selectedCurrencyInfo: CurrencyInfo amount: number + amountReady?: boolean disabled?: boolean loading?: boolean } @@ -229,6 +242,7 @@ function SelectTokenButton({ selectedCurrencyInfo, onPress, amount, + amountReady, disabled, loading, }: SelectTokenButtonProps): JSX.Element { @@ -236,12 +250,13 @@ function SelectTokenButton({ amount.toString(), selectedCurrencyInfo.currency ) - const textColor = disabled || loading ? '$neutral3' : '$neutral2' + const textColor = !amountReady || disabled || loading ? '$neutral3' : '$neutral2' return ( @@ -272,11 +287,13 @@ function PredefinedAmount({ onPress, currentAmount, fiatCurrencyInfo, + disabled, }: { amount: number currentAmount: string onPress: (amount: string) => void fiatCurrencyInfo: FiatCurrencyInfo + disabled?: boolean }): JSX.Element { const colors = useSporeColors() const { addFiatSymbolToNumber } = useLocalizationContext() @@ -290,14 +307,15 @@ function PredefinedAmount({ return ( => { await HapticFeedback.impact() onPress(amount.toString()) }}> void }): JSX.Element { - + width={ServiceProviderLogoStyles.icon.width}> + } serviceProviderName="MoonPay" diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampTransferInstitutionSelector.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampTransferInstitutionSelector.tsx deleted file mode 100644 index 6f3798d8498..00000000000 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampTransferInstitutionSelector.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { BottomSheetFlatList } from '@gorhom/bottom-sheet' -import React, { useCallback } from 'react' -import { ListRenderItemInfo } from 'react-native' -import { getCountry } from 'react-native-localize' -import { FadeIn, FadeOut } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' -import { openModal } from 'src/features/modals/modalSlice' -import { AnimatedFlex, Flex, ImpactFeedbackStyle, Loader, Text, TouchableArea } from 'ui/src' -import { iconSizes } from 'ui/src/theme' -import { useFiatOnRampAggregatorTransferInstitutionsQuery } from 'wallet/src/features/fiatOnRamp/api' -import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' -import { RemoteImage } from 'wallet/src/features/images/RemoteImage' -import { ModalName } from 'wallet/src/telemetry/constants' - -function key(item: FORTransferInstitution): string { - return item.id as string -} - -const CEX_ICON_SIZE = iconSizes.icon36 -const CEX_ICON_BORDER_RADIUS = 12 - -function CEXItemWrapper({ - institution, - onSelectTransferInstitution, -}: { - institution: FORTransferInstitution - onSelectTransferInstitution: (transferInstitution: FORTransferInstitution) => void -}): JSX.Element | null { - const onPress = (): void => onSelectTransferInstitution(institution) - - return ( - - - - - - {institution.name} - - - - - ) -} - -export function TransferInstitutionSelector({ onClose }: { onClose: () => void }): JSX.Element { - const dispatch = useAppDispatch() - const { data, isLoading } = useFiatOnRampAggregatorTransferInstitutionsQuery({ - countryCode: getCountry(), - }) - - const onSelectTransferInstitution = useCallback( - (transferInstitution: FORTransferInstitution) => { - dispatch( - openModal({ - name: ModalName.ExchangeTransferModal, - initialState: { serviceProvider: transferInstitution }, - }) - ) - onClose() - }, - [dispatch, onClose] - ) - - const renderItem = useCallback( - ({ item: institution }: ListRenderItemInfo) => ( - - ), - [onSelectTransferInstitution] - ) - - return ( - - - {isLoading ? ( - - ) : ( - - )} - - - ) -} - -const renderItemSeparator = (): JSX.Element => diff --git a/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts b/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts index 3d1b43603e2..5af94b97992 100644 --- a/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts +++ b/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts @@ -15,7 +15,7 @@ import { useFiatOnRampAggregatorCryptoQuoteQuery, useFiatOnRampAggregatorSupportedFiatCurrenciesQuery, } from 'wallet/src/features/fiatOnRamp/api' -import { FORQuote } from 'wallet/src/features/fiatOnRamp/types' +import { FORQuote, FORSupportedFiatCurrency } from 'wallet/src/features/fiatOnRamp/types' import { isFiatOnRampApiError, isInvalidRequestAmountTooHigh, @@ -27,6 +27,7 @@ import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' export function useMeldFiatCurrencySupportInfo(countryCode: string): { appFiatCurrencySupportedInMeld: boolean meldSupportedFiatCurrency: FiatCurrencyInfo + supportedFiatCurrencies: FORSupportedFiatCurrency[] | undefined } { // Not all the currencies are supported by Meld, so we need to fallback to USD if the currency is not supported const appFiatCurrencyInfo = useAppFiatCurrencyInfo() @@ -49,6 +50,7 @@ export function useMeldFiatCurrencySupportInfo(countryCode: string): { return { appFiatCurrencySupportedInMeld: appFiatCurrencySupported, meldSupportedFiatCurrency, + supportedFiatCurrencies: supportedFiatCurrencies?.fiatCurrencies, } } diff --git a/apps/mobile/src/features/fiatOnRamp/constants.ts b/apps/mobile/src/features/fiatOnRamp/constants.ts index 5345144bf82..02934f40bf1 100644 --- a/apps/mobile/src/features/fiatOnRamp/constants.ts +++ b/apps/mobile/src/features/fiatOnRamp/constants.ts @@ -1 +1,12 @@ +import { StyleSheet } from 'react-native' + export const FOR_MODAL_SNAP_POINTS = ['70%', '100%'] +export const SERVICE_PROVIDER_ICON_SIZE = 90 +export const SERVICE_PROVIDER_ICON_BORDER_RADIUS = 20 + +export const ServiceProviderLogoStyles = StyleSheet.create({ + icon: { + height: SERVICE_PROVIDER_ICON_SIZE, + width: SERVICE_PROVIDER_ICON_SIZE, + }, +}) diff --git a/apps/mobile/src/features/fiatOnRamp/hooks.ts b/apps/mobile/src/features/fiatOnRamp/hooks.ts index 7f44a07a0eb..73c028109af 100644 --- a/apps/mobile/src/features/fiatOnRamp/hooks.ts +++ b/apps/mobile/src/features/fiatOnRamp/hooks.ts @@ -8,7 +8,6 @@ import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' import { ColorTokens, useSporeColors } from 'ui/src' import { uniswapUrls } from 'uniswap/src/constants/urls' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { isAndroid } from 'uniswap/src/utils/platform' import { logger } from 'utilities/src/logger/logger' import { useDebounce } from 'utilities/src/time/timing' import { @@ -194,9 +193,7 @@ export function useMoonpayFiatOnRamp({ amount: baseCurrencyAmount, currencyCode: quoteCurrencyCode, baseCurrencyCode, - redirectUrl: `${ - isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl - }/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, + redirectUrl: `${uniswapUrls.redirectUrlBase}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, } : skipToken ) diff --git a/apps/mobile/src/features/firebase/utils.ts b/apps/mobile/src/features/firebase/utils.ts index e23ec4d0042..862644745ee 100644 --- a/apps/mobile/src/features/firebase/utils.ts +++ b/apps/mobile/src/features/firebase/utils.ts @@ -1,7 +1,7 @@ import type { ReactNativeFirebase } from '@react-native-firebase/app' import '@react-native-firebase/auth' import firestore, { FirebaseFirestoreTypes } from '@react-native-firebase/firestore' -import { isBetaBuild, isDevBuild } from 'src/utils/version' +import { isBetaEnv, isDevEnv } from 'uniswap/src/utils/env' const ADDRESS_DATA_COLLECTION = 'address_data' const DEV_ADDRESS_DATA_COLLECTION = 'dev_address_data' @@ -39,10 +39,10 @@ export const getFirestoreMetadataRef = ( .doc('data') export function getAddressDataCollectionFromBundleId(): string { - if (isDevBuild()) { + if (isDevEnv()) { return DEV_ADDRESS_DATA_COLLECTION } - if (isBetaBuild()) { + if (isBetaEnv()) { return BETA_ADDRESS_DATA_COLLECTION } return ADDRESS_DATA_COLLECTION diff --git a/apps/mobile/src/features/modals/ModalsState.ts b/apps/mobile/src/features/modals/ModalsState.ts index 05efa6b6d5d..6dee1350557 100644 --- a/apps/mobile/src/features/modals/ModalsState.ts +++ b/apps/mobile/src/features/modals/ModalsState.ts @@ -2,9 +2,10 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { ExtensionWaitlistModalState } from 'src/features/scantastic/ExtensionWaitlistModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' +import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { Screens } from 'src/screens/Screens' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' -import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' +import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' import { ModalName } from 'wallet/src/telemetry/constants' @@ -16,14 +17,14 @@ export interface AppModalState { export interface ModalsState { [ModalName.AccountSwitcher]: AppModalState [ModalName.ExchangeTransferModal]: AppModalState<{ - serviceProvider: FORTransferInstitution + serviceProvider: FORServiceProvider }> [ModalName.Experiments]: AppModalState [ModalName.Explore]: AppModalState [ModalName.FiatCurrencySelector]: AppModalState [ModalName.FiatOnRamp]: AppModalState [ModalName.FiatOnRampAggregator]: AppModalState - [ModalName.ReceiveCryptoModal]: AppModalState + [ModalName.ReceiveCryptoModal]: AppModalState [ModalName.LanguageSelector]: AppModalState [ModalName.RemoveWallet]: AppModalState [ModalName.RestoreWallet]: AppModalState diff --git a/apps/mobile/src/features/modals/modalSlice.ts b/apps/mobile/src/features/modals/modalSlice.ts index 10be0e6cdd6..5966bc25558 100644 --- a/apps/mobile/src/features/modals/modalSlice.ts +++ b/apps/mobile/src/features/modals/modalSlice.ts @@ -4,6 +4,7 @@ import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWallet import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState' import { ExtensionWaitlistModalState } from 'src/features/scantastic/ExtensionWaitlistModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' +import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { Screens } from 'src/screens/Screens' import { getKeys } from 'utilities/src/primitives/objects' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' @@ -47,7 +48,7 @@ type FiatOnRampAggregatorModalParams = { type ReceiveCryptoModalParams = { name: typeof ModalName.ReceiveCryptoModal - initialState?: undefined + initialState: ReceiveCryptoModalState } type LanguageSelectorModalParams = { @@ -123,7 +124,7 @@ export const initialModalsState: ModalsState = { }, [ModalName.ReceiveCryptoModal]: { isOpen: false, - initialState: undefined, + initialState: [], }, [ModalName.WalletConnectScan]: { isOpen: false, diff --git a/apps/mobile/src/features/onboarding/hooks.ts b/apps/mobile/src/features/onboarding/hooks.ts index e6613b00a0c..66cd8914dd3 100644 --- a/apps/mobile/src/features/onboarding/hooks.ts +++ b/apps/mobile/src/features/onboarding/hooks.ts @@ -4,8 +4,8 @@ import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { MobileEventName } from 'src/features/telemetry/constants' import { Screens } from 'src/screens/Screens' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' import { setHasSkippedUnitagPrompt, @@ -43,7 +43,6 @@ export function useCompleteOnboardingCallback({ const parentTrace = useTrace() const navigation = useOnboardingStackNavigation() - const unitagsFeatureFlagEnabled = useFeatureFlag(FeatureFlags.Unitags) const claimUnitag = useClaimUnitag() const uniconsV2Enabled = useFeatureFlag(FeatureFlags.UniconsV2) @@ -90,10 +89,8 @@ export function useCompleteOnboardingCallback({ // Remove pending flag from all new accounts. dispatch(pendingAccountActions.trigger(PendingAccountActions.Activate)) - // Dismiss unitags prompt if: - // - the feature was enabled - // - the onboarding method prompts for unitags (create new) - if (unitagsFeatureFlagEnabled && importType === ImportType.CreateNew) { + // Dismiss unitags prompt if the onboarding method prompts for unitags (create new) + if (importType === ImportType.CreateNew) { dispatch(setHasSkippedUnitagPrompt(true)) } diff --git a/apps/mobile/src/features/scantastic/ScantasticModal.tsx b/apps/mobile/src/features/scantastic/ScantasticModal.tsx index c2eccd38c1b..2a31837b523 100644 --- a/apps/mobile/src/features/scantastic/ScantasticModal.tsx +++ b/apps/mobile/src/features/scantastic/ScantasticModal.tsx @@ -121,7 +121,7 @@ export function ScantasticModal(): JSX.Element | null { try { // submit encrypted blob - const response = await fetch(`${uniswapUrls.apiBaseExtensionUrl}/scantastic/blob`, { + const response = await fetch(`${uniswapUrls.scantasticApiUrl}/blob`, { method: 'POST', headers: { Accept: 'application/json', @@ -176,16 +176,13 @@ export function ScantasticModal(): JSX.Element | null { return } try { - const response = await fetch( - `${uniswapUrls.apiBaseExtensionUrl}/scantastic/otp-state/${uuid}`, - { - method: 'POST', - headers: { - Accept: 'application/json', - Origin: 'https://uniswap.org', - }, - } - ) + const response = await fetch(`${uniswapUrls.scantasticApiUrl}/otp-state/${uuid}`, { + method: 'POST', + headers: { + Accept: 'application/json', + Origin: 'https://uniswap.org', + }, + }) if (!response.ok) { throw new Error(`Failed to check OTP state: ${await response.text()}`) } diff --git a/apps/mobile/src/features/telemetry/saga.ts b/apps/mobile/src/features/telemetry/saga.ts index 1427d1b1065..002fea4a91d 100644 --- a/apps/mobile/src/features/telemetry/saga.ts +++ b/apps/mobile/src/features/telemetry/saga.ts @@ -16,7 +16,7 @@ export function* telemetrySaga() { new ApplicationTransport( uniswapUrls.amplitudeProxyUrl, OriginApplication.MOBILE, - uniswapUrls.apiBaseUrl, + uniswapUrls.apiOrigin, DeviceInfo.getBundleId() ), allowAnalytics diff --git a/apps/mobile/src/features/telemetry/types.ts b/apps/mobile/src/features/telemetry/types.ts index e1b3d936014..5e12b79d2f9 100644 --- a/apps/mobile/src/features/telemetry/types.ts +++ b/apps/mobile/src/features/telemetry/types.ts @@ -4,7 +4,12 @@ import { MobileEventName } from 'src/features/telemetry/constants' import { WidgetEvent, WidgetType } from 'src/features/widgets/widgets' import { TraceProps } from 'utilities/src/telemetry/trace/Trace' import { ImportType } from 'wallet/src/features/onboarding/types' -import { EthMethod, WCEventType, WCRequestOutcome } from 'wallet/src/features/walletConnect/types' +import { + EthMethod, + UwULinkMethod, + WCEventType, + WCRequestOutcome, +} from 'wallet/src/features/walletConnect/types' import { ShareableEntity } from 'wallet/src/telemetry/constants' // Events related to Moonpay internal transactions @@ -86,7 +91,7 @@ export type MobileEventProperties = { [MobileEventName.WalletAdded]: OnboardingCompletedProps & TraceProps [MobileEventName.WalletConnectSheetCompleted]: { request_type: WCEventType - eth_method?: EthMethod + eth_method?: EthMethod | UwULinkMethod dapp_url: string dapp_name: string wc_version: string diff --git a/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx index de81c15bd67..99f2b3540b6 100644 --- a/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx +++ b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx @@ -28,7 +28,7 @@ import { useParsedSendWarnings } from 'wallet/src/features/transactions/hooks/us import { useTokenSelectorActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers' import { useTransactionGasWarning } from 'wallet/src/features/transactions/hooks/useTransactionGasWarning' import { - initialState as emptyState, + INITIAL_TRANSACTION_STATE, transactionStateReducer, } from 'wallet/src/features/transactions/transactionState/transactionState' import { @@ -67,7 +67,10 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS const { fullWidth } = useDeviceDimensions() const { isSheetReady } = useBottomSheetContext() - const [state, dispatch] = useReducer(transactionStateReducer, prefilledState || emptyState) + const [state, dispatch] = useReducer( + transactionStateReducer, + prefilledState || INITIAL_TRANSACTION_STATE + ) const derivedTransferInfo = useDerivedTransferInfo(state) const [showViewOnlyModal, setShowViewOnlyModal] = useState(false) const [step, setStep] = useState(TransactionStep.FORM) @@ -89,9 +92,8 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS ) const transferTxWithGasSettings = useMemo( - (): providers.TransactionRequest | undefined => - gasFee?.params ? { ...txRequest, ...gasFee.params } : txRequest, - [gasFee?.params, txRequest] + (): providers.TransactionRequest => ({ ...txRequest, ...gasFee.params }), + [gasFee.params, txRequest] ) const gasWarning = useTransactionGasWarning({ diff --git a/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx index aeeb6a2bdab..50e7f8620d6 100644 --- a/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx +++ b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx @@ -297,7 +297,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { borderWidth={0} fontFamily="$heading" fontSize={fontSize} - fontWeight="$large" + fontWeight="$medium" numberOfLines={1} p="$none" placeholder={inputPlaceholder} diff --git a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx index 4fb269135aa..338712308ea 100644 --- a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx +++ b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx @@ -28,9 +28,10 @@ import { } from 'ui/src' import { borderRadii, fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' import { useExtractedColors } from 'ui/src/utils/colors' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useUnitagUpdater } from 'uniswap/src/features/unitags/context' +import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { ProfileMetadata } from 'uniswap/src/features/unitags/types' import { isIOS } from 'uniswap/src/utils/platform' import { logger } from 'utilities/src/logger/logger' @@ -43,10 +44,7 @@ import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { updateUnitagMetadata } from 'wallet/src/features/unitags/api' import { tryUploadAvatar } from 'wallet/src/features/unitags/avatars' -import { - useAvatarUploadCredsWithRefresh, - useUnitagByAddress, -} from 'wallet/src/features/unitags/hooks' +import { useAvatarUploadCredsWithRefresh } from 'wallet/src/features/unitags/hooks' import { useWalletSigners } from 'wallet/src/features/wallet/context' import { useAccount } from 'wallet/src/features/wallet/hooks' import { DisplayNameType } from 'wallet/src/features/wallet/types' diff --git a/apps/mobile/src/features/wallet/hooks.ts b/apps/mobile/src/features/wallet/hooks.ts index be8b623ba85..0d4ab26fa5c 100644 --- a/apps/mobile/src/features/wallet/hooks.ts +++ b/apps/mobile/src/features/wallet/hooks.ts @@ -2,8 +2,8 @@ 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 { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { logger } from 'utilities/src/logger/logger' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' diff --git a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts index b1bbd488448..91b808c6020 100644 --- a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts +++ b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts @@ -82,7 +82,7 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify({ method: 'eth_sendTransaction', response: signature }), + body: JSON.stringify({ method: 'eth_sendTransaction', response: signature, chainId }), // TODO: consider adding analytics to track UwuLink usage }).catch((error) => logger.error(error, { @@ -107,7 +107,7 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams type: AppNotificationType.WalletConnect, event: WalletConnectEvent.TransactionFailed, dappName: params.dapp.name, - imageUrl: params.dapp.icon, + imageUrl: params.dapp.icon ?? null, chainId, address: account.address, }) diff --git a/apps/mobile/src/features/walletConnect/walletConnectSlice.ts b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts index cb3bc3e413d..49cd0308630 100644 --- a/apps/mobile/src/features/walletConnect/walletConnectSlice.ts +++ b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts @@ -6,6 +6,7 @@ import { EthMethod, EthSignMethod, EthTransaction, + UwULinkMethod, } from 'wallet/src/features/walletConnect/types' export type WalletConnectPendingSession = { @@ -45,11 +46,24 @@ export interface TransactionRequest extends BaseRequest { transaction: EthTransaction } -export type WalletConnectRequest = SignRequest | TransactionRequest +export interface UwuLinkErc20Request extends BaseRequest { + type: UwULinkMethod.Erc20Send + recipient: { + address: string + name: string + } + tokenAddress: string + amount: string + isStablecoin: boolean + transaction: EthTransaction // the formatted transaction, prepared by the wallet +} + +export type WalletConnectRequest = SignRequest | TransactionRequest | UwuLinkErc20Request export const isTransactionRequest = ( request: WalletConnectRequest -): request is TransactionRequest => request.type === EthMethod.EthSendTransaction +): request is TransactionRequest => + request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send export interface WalletConnectState { byAccount: { diff --git a/apps/mobile/src/polyfills/index.ts b/apps/mobile/src/polyfills/index.ts index 5366f6c1346..80f19c07119 100644 --- a/apps/mobile/src/polyfills/index.ts +++ b/apps/mobile/src/polyfills/index.ts @@ -8,7 +8,7 @@ // Import the crypto getRandomValues shim BEFORE ethers shims import 'react-native-get-random-values' -// Import the the ethers shims BEFORE ethers +// Import the ethers shims BEFORE ethers import '@ethersproject/shims' // Add .at() method to Array if necessary (missing before iOS 15) import 'src/polyfills/arrayAt' diff --git a/apps/mobile/src/screens/ExchangeTransferConnecting.tsx b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx index f1f5eb3bd95..e6edcfe6b54 100644 --- a/apps/mobile/src/screens/ExchangeTransferConnecting.tsx +++ b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx @@ -3,20 +3,18 @@ import { useTranslation } from 'react-i18next' import { getCountry } from 'react-native-localize' import { useAppDispatch } from 'src/app/hooks' import { Screen } from 'src/components/layout/Screen' -import { - FiatOnRampConnectingView, - SERVICE_PROVIDER_ICON_BORDER_RADIUS, - SERVICE_PROVIDER_ICON_SIZE, -} from 'src/features/fiatOnRamp/FiatOnRampConnecting' +import { FiatOnRampConnectingView } from 'src/features/fiatOnRamp/FiatOnRampConnecting' +import { ServiceProviderLogoStyles } from 'src/features/fiatOnRamp/constants' import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks' +import { Flex, useIsDarkMode } from 'ui/src' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { isAndroid } from 'uniswap/src/utils/platform' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useTimeout } from 'utilities/src/time/timing' import { ChainId } from 'wallet/src/constants/chains' import { useFiatOnRampAggregatorTransferWidgetQuery } from 'wallet/src/features/fiatOnRamp/api' -import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' -import { RemoteImage } from 'wallet/src/features/images/RemoteImage' +import { FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' +import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { ImageUri } from 'wallet/src/features/images/ImageUri' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' @@ -27,14 +25,11 @@ import { openUri } from 'wallet/src/utils/linking' // Design decision const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS -const DEFAULT_TRANSFER_AMOUNT = 1 -const DEFAULT_TRANSFER_CURRENCY = 'ETH' - export function ExchangeTransferConnecting({ serviceProvider, onClose, }: { - serviceProvider: FORTransferInstitution + serviceProvider: FORServiceProvider onClose: () => void }): JSX.Element { const { t } = useTranslation() @@ -43,8 +38,8 @@ export function ExchangeTransferConnecting({ const [timeoutElapsed, setTimeoutElapsed] = useState(false) const initialTypeInfo = useMemo( - () => ({ institutionLogoUrl: serviceProvider.icon }), - [serviceProvider.icon] + () => ({ serviceProviderLogo: serviceProvider.logos }), + [serviceProvider.logos] ) const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator( @@ -72,15 +67,11 @@ export function ExchangeTransferConnecting({ isLoading: widgetLoading, error: widgetError, } = useFiatOnRampAggregatorTransferWidgetQuery({ - sourceAmount: DEFAULT_TRANSFER_AMOUNT, - sourceCurrencyCode: DEFAULT_TRANSFER_CURRENCY, countryCode: getCountry(), - institutionId: serviceProvider.id, + serviceProvider: serviceProvider.serviceProvider, walletAddress: activeAccountAddress, externalSessionId: externalTransactionId, - redirectURL: `${ - isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl - }/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, + redirectUrl: `${uniswapUrls.redirectUrlBase}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, }) useEffect(() => { @@ -113,16 +104,20 @@ export function ExchangeTransferConnecting({ serviceProvider?.name, ]) + const isDarkMode = useIsDarkMode() + const logoUrl = getServiceProviderLogo(serviceProvider.logos, isDarkMode) + return ( + + + } serviceProviderName={serviceProvider.name} /> diff --git a/apps/mobile/src/screens/FiatOnRampConnecting.tsx b/apps/mobile/src/screens/FiatOnRampConnecting.tsx index 677415e536f..6b2823e809d 100644 --- a/apps/mobile/src/screens/FiatOnRampConnecting.tsx +++ b/apps/mobile/src/screens/FiatOnRampConnecting.tsx @@ -2,15 +2,12 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import { skipToken } from '@reduxjs/toolkit/query/react' import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { StyleSheet } from 'react-native' import { useAppDispatch } from 'src/app/hooks' import { FiatOnRampStackParamList } from 'src/app/navigation/types' import { Screen } from 'src/components/layout/Screen' -import { - FiatOnRampConnectingView, - SERVICE_PROVIDER_ICON_SIZE, -} from 'src/features/fiatOnRamp/FiatOnRampConnecting' +import { FiatOnRampConnectingView } from 'src/features/fiatOnRamp/FiatOnRampConnecting' import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' +import { ServiceProviderLogoStyles } from 'src/features/fiatOnRamp/constants' import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks' import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils' import { closeModal } from 'src/features/modals/modalSlice' @@ -18,12 +15,11 @@ import { FiatOnRampScreens } from 'src/screens/Screens' import { Flex, Text, useIsDarkMode } from 'ui/src' import { spacing } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { isAndroid } from 'uniswap/src/utils/platform' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useTimeout } from 'utilities/src/time/timing' import { ChainId } from 'wallet/src/constants/chains' import { useFiatOnRampAggregatorWidgetQuery } from 'wallet/src/features/fiatOnRamp/api' -import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { getOptionalServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' import { ImageUri } from 'wallet/src/features/images/ImageUri' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { pushNotification } from 'wallet/src/features/notifications/slice' @@ -93,9 +89,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | sourceCurrencyCode: baseCurrencyInfo.code, walletAddress: activeAccountAddress, externalSessionId: externalTransactionId, - redirectUrl: `${ - isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl - }/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, + redirectUrl: `${uniswapUrls.redirectUrlBase}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, } : skipToken ) @@ -154,7 +148,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ]) const isDarkMode = useIsDarkMode() - const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode) + const logoUrl = getOptionalServiceProviderLogo(serviceProvider?.logos, isDarkMode) return ( @@ -170,9 +164,9 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | serviceProviderLogo={ + width={ServiceProviderLogoStyles.icon.width}> } @@ -192,10 +186,3 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ) } - -const ServiceProviderLogoStyles = StyleSheet.create({ - icon: { - height: SERVICE_PROVIDER_ICON_SIZE, - width: SERVICE_PROVIDER_ICON_SIZE, - }, -}) diff --git a/apps/mobile/src/screens/FiatOnRampScreen.tsx b/apps/mobile/src/screens/FiatOnRampScreen.tsx index 64b6d0be4b5..45626bc6991 100644 --- a/apps/mobile/src/screens/FiatOnRampScreen.tsx +++ b/apps/mobile/src/screens/FiatOnRampScreen.tsx @@ -33,7 +33,11 @@ import { useFiatOnRampAggregatorTransactionsQuery, } from 'wallet/src/features/fiatOnRamp/api' import { FORQuote, FORServiceProvider, FORTransaction } from 'wallet/src/features/fiatOnRamp/types' -import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { + getServiceProviderLogo, + isFiatOnRampApiError, + isNoQuotesError, +} from 'wallet/src/features/fiatOnRamp/utils' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' @@ -116,7 +120,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = useShouldShowNativeKeyboard() - const { appFiatCurrencySupportedInMeld, meldSupportedFiatCurrency } = + const { appFiatCurrencySupportedInMeld, meldSupportedFiatCurrency, supportedFiatCurrencies } = useMeldFiatCurrencySupportInfo(countryCode) const debouncedAmount = useDebounce(amount, DEFAULT_DELAY * 2) @@ -164,11 +168,6 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { limit: 1, }) - const { errorText, errorColor } = useParseFiatOnRampError( - quotesError || serviceProvidersError, - meldSupportedFiatCurrency.code - ) - const prevQuotes = usePrevious(quotes) useEffect(() => { if (quotes && (!selectedQuote || prevQuotes !== quotes)) { @@ -285,6 +284,16 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { meldSupportedFiatCurrency.code.toLowerCase() ) + const notAvailableInThisRegion = + supportedFiatCurrencies?.length === 0 || + (isFiatOnRampApiError(quotesError) && isNoQuotesError(quotesError)) || + quotes?.length === 0 + + const { errorText, errorColor } = useParseFiatOnRampError( + !notAvailableInThisRegion && (quotesError || serviceProvidersError), + meldSupportedFiatCurrency.code + ) + return ( @@ -312,6 +321,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { errorText={errorText} fiatCurrencyInfo={meldSupportedFiatCurrency} inputRef={inputRef} + notAvailableInThisRegion={notAvailableInThisRegion} predefinedAmountsSupported={predefinedAmountsSupported} quoteAmount={selectedQuote?.destinationAmount ?? 0} quoteCurrencyAmountReady={Boolean(amount && selectedQuote)} @@ -341,6 +351,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { {!showNativeKeyboard && ( ): JSX.Elemen const forAggregatorEnabled = useFeatureFlag(FeatureFlags.ForAggregator) const cexTransferEnabled = useFeatureFlag(FeatureFlags.CexTransfers) + const cexTransferProviders = useCexTransferProviders(cexTransferEnabled) const onPressBuy = useCallback( () => @@ -351,14 +353,14 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen }, [dispatch]) const onPressSend = useCallback(() => dispatch(openModal({ name: ModalName.Send })), [dispatch]) const onPressReceive = useCallback(() => { - if (cexTransferEnabled) { - dispatch(openModal({ name: ModalName.ReceiveCryptoModal })) - } else { - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) + dispatch( + openModal( + cexTransferProviders.length > 0 + ? { name: ModalName.ReceiveCryptoModal, initialState: cexTransferProviders } + : { name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr } ) - } - }, [dispatch, cexTransferEnabled]) + ) + }, [dispatch, cexTransferProviders]) const onPressViewOnlyLabel = useCallback( () => dispatch(openModal({ name: ModalName.ViewOnlyExplainer })), [dispatch] diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx index 788d43c55d0..a4261675119 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx @@ -11,8 +11,8 @@ import { OnboardingScreens } from 'src/screens/Screens' import { useAddBackButton } from 'src/utils/useAddBackButton' import { Flex, Icons, Text, TouchableArea, Unicon, UniconV2, useIsDarkMode } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/experiments/flags' -import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' import { FORMAT_DATE_TIME_SHORT, diff --git a/apps/mobile/src/screens/NFTItemScreen.tsx b/apps/mobile/src/screens/NFTItemScreen.tsx index 305c725bb59..ed2d7671495 100644 --- a/apps/mobile/src/screens/NFTItemScreen.tsx +++ b/apps/mobile/src/screens/NFTItemScreen.tsx @@ -13,7 +13,6 @@ import { Loader } from 'src/components/loading' import { LongMarkdownText } from 'src/components/text/LongMarkdownText' import { selectModalState } from 'src/features/modals/selectModalState' import { PriceAmount } from 'src/features/nfts/collection/ListPriceCard' -import { useNFTMenu } from 'src/features/nfts/hooks' import { BlurredImageBackground } from 'src/features/nfts/item/BlurredImageBackground' import { CollectionPreviewCard } from 'src/features/nfts/item/CollectionPreviewCard' import { NFTTraitList } from 'src/features/nfts/item/traits' @@ -43,6 +42,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { PollingInterval } from 'wallet/src/constants/misc' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' +import { useNFTContextMenu } from 'wallet/src/features/nfts/useNftContextMenu' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' @@ -422,7 +422,7 @@ function RightElement({ }): JSX.Element { const colors = useSporeColors() - const { menuActions, onContextMenuPress, onlyShare } = useNFTMenu({ + const { menuActions, onContextMenuPress, onlyShare } = useNFTContextMenu({ contractAddress: asset?.nftContract?.address, tokenId: asset?.tokenId, owner, diff --git a/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx b/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx deleted file mode 100644 index ceda130ef74..00000000000 --- a/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' -import { Trans, useTranslation } from 'react-i18next' -import { ActivityIndicator, TextInput as NativeTextInput, StyleSheet } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' -import { OnboardingStackParamList } from 'src/app/navigation/types' -import Trace from 'src/components/Trace/Trace' -import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' -import { OnboardingScreens } from 'src/screens/Screens' -import { useAddBackButton } from 'src/utils/useAddBackButton' -import { AnimatePresence, Button, Flex, Icons, Text } from 'ui/src' -import { fonts } from 'ui/src/theme' -import { isAndroid } from 'uniswap/src/utils/platform' -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 { - EditAccountAction, - editAccountActions, -} from 'wallet/src/features/wallet/accounts/editAccountSaga' -import { AccountType } from 'wallet/src/features/wallet/accounts/types' -import { - PendingAccountActions, - 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' - -type Props = NativeStackScreenProps - -export function EditNameScreen({ navigation, route: { params } }: Props): JSX.Element { - const dispatch = useAppDispatch() - const { t } = useTranslation() - - // Reference pending accounts to avoid any lag in saga import. - const pendingAccount = Object.values(usePendingAccounts())?.[0] - - useEffect(() => { - // Sets the default wallet nickname based on derivation index once the pendingAccount is set. - if (pendingAccount && pendingAccount.type !== AccountType.Readonly) { - setNewAccountName( - pendingAccount.name || - t('onboarding.wallet.defaultName', { number: pendingAccount.derivationIndex + 1 }) || - '' - ) - } - }, [pendingAccount, t]) - - const [newAccountName, setNewAccountName] = useState('') - - useAddBackButton(navigation) - - useEffect(() => { - const beforeRemoveListener = (): void => { - dispatch(pendingAccountActions.trigger(PendingAccountActions.Delete)) - } - navigation.addListener('beforeRemove', beforeRemoveListener) - - return () => navigation.removeListener('beforeRemove', beforeRemoveListener) - }, [dispatch, navigation]) - - const onPressNext = (): void => { - navigation.navigate({ - name: - params?.importType === ImportType.CreateNew - ? OnboardingScreens.WelcomeWallet - : OnboardingScreens.Notifications, - merge: true, - params, - }) - - if (pendingAccount) { - dispatch( - editAccountActions.trigger({ - type: EditAccountAction.Rename, - address: pendingAccount?.address, - newName: newAccountName.trim(), - }) - ) - } - } - - return ( - - {pendingAccount ? ( - - ) : ( - - )} - - - - - - - ) -} - -function CustomizationSection({ - address, - accountName, - setAccountName, -}: { - address: Address - accountName: string - setAccountName: Dispatch> -}): JSX.Element { - const { t } = useTranslation() - const textInputRef = useRef(null) - - // we default it to `true` to avoid flickering of a pencil icon, - // because CustomizationSection has `autoFocus=true` - const [focused, setFocused] = useState(true) - - const focusInputWithKeyboard = (): void => { - textInputRef.current?.focus() - } - const walletAddress = shortenAddress(address) - - return ( - - - - - { - setFocused(false) - setAccountName(accountName.trim()) - }} - onChangeText={setAccountName} - onFocus={(): void => setFocused(true)} - /> - - {!focused && ( -