From 831cd5d66491fcd22bf273484b1c166ad75beb2f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 19:00:45 -0400 Subject: [PATCH] 5.2.0 (#4341) --- CHANGELOG.md | 23 +- android/app/build.gradle | 4 +- app/components/UI/AccountOverview/index.js | 16 +- app/components/UI/AssetOverview/index.js | 16 +- .../Views/AmountToBuy.tsx | 182 +++++++++------- .../FiatOnRampAggregator/Views/Checkout.tsx | 70 ++++-- .../FiatOnRampAggregator/Views/GetQuotes.tsx | 172 +++++++++++++-- .../FiatOnRampAggregator/Views/GetStarted.tsx | 15 +- .../Views/OrderDetails.tsx | 31 ++- .../Views/PaymentMethod.tsx | 52 +++-- .../UI/FiatOnRampAggregator/Views/Region.tsx | 14 +- .../components/ErrorViewWithReporting.tsx | 5 +- .../components/InfoAlert.tsx | 59 ++++- ...ransactionDetails.tsx => OrderDetails.tsx} | 201 +++++++++++------- .../components/PaymentMethodModal.tsx | 19 +- .../components/RegionAlert.tsx | 10 +- .../components/RegionModal.tsx | 18 +- .../containers/ApplePayButton.tsx | 38 +++- .../hooks/useAnalytics.ts | 43 ++++ .../orderProcessor/aggregator.ts | 8 +- .../UI/FiatOnRampAggregator/sdk/index.tsx | 10 + .../FiatOnRampAggregator/types/analytics.ts | 130 +++++++++++ .../UI/FiatOnRampAggregator/types/index.ts | 2 + .../UI/FiatOrders/MoonPayWebView/index.js | 2 +- .../FiatOrders/PaymentMethodApplePay/index.js | 2 +- .../UI/FiatOrders/TransakWebView/index.js | 2 +- app/components/UI/FiatOrders/index.js | 64 +++++- app/components/UI/Navbar/index.js | 7 +- app/components/UI/ReceiveRequest/index.js | 16 +- .../__snapshots__/index.test.tsx.snap | 4 + app/components/UI/SelectComponent/index.js | 3 +- .../Tokens/__snapshots__/index.test.tsx.snap | 3 + app/components/UI/Tokens/index.js | 18 +- .../Views/RevealPrivateCredential/index.js | 28 +-- app/components/Views/SendFlow/SendTo/index.js | 86 +++++--- app/core/DeeplinkManager.js | 8 +- app/util/analytics.js | 8 - app/util/analyticsV2.js | 33 ++- bitrise.yml | 4 +- ios/MetaMask.xcodeproj/project.pbxproj | 8 +- locales/languages/el.json | 4 + locales/languages/en.json | 30 +-- locales/languages/fr.json | 4 + locales/languages/hi.json | 5 + locales/languages/id.json | 5 + locales/languages/ja.json | 5 + locales/languages/ko.json | 5 + locales/languages/pt.json | 5 + locales/languages/ru.json | 5 + locales/languages/tr.json | 4 + locales/languages/vi.json | 5 + package.json | 4 +- yarn.lock | 8 +- 53 files changed, 1156 insertions(+), 367 deletions(-) rename app/components/UI/FiatOnRampAggregator/components/{TransactionDetails.tsx => OrderDetails.tsx} (64%) create mode 100644 app/components/UI/FiatOnRampAggregator/hooks/useAnalytics.ts create mode 100644 app/components/UI/FiatOnRampAggregator/types/analytics.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d6468a8980..a6248f1bdb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,24 @@ ## Current Main Branch -## 5.1.1 - May 12, 2022 -- [#4254](https://github.com/MetaMask/metamask-mobile/pull/4254): [FIX] Crashes on Android devices when using the app in split screen -- [#4052](https://github.com/MetaMask/metamask-mobile/pull/4052): [FEAT] Download attachments in browser on iOS -- [4174](https://github.com/MetaMask/metamask-mobile/pull/4174): [IMPROVEMENT] Use checksum standard format -- [#4318](https://github.com/MetaMask/metamask-mobile/pull/4318): [FIX] Send flow confusables bug -- [#4315](https://github.com/MetaMask/metamask-mobile/pull/4315): [IMPROVEMENT] Make reveal SRP blur view dynamic +## 5.2.0 - May 17, 2022 - [#4349](https://github.com/MetaMask/metamask-mobile/pull/4349): [FIX] Subtitle mapping +- [#4344](https://github.com/MetaMask/metamask-mobile/pull/4344): [FIX] Fix homepage scripts and env import +- [#4345](https://github.com/MetaMask/metamask-mobile/pull/4345): [FIX] Fix check for empty tokens list +- [#3696](https://github.com/MetaMask/metamask-mobile/pull/3696): [FEATURE] Fiat on Ramp Aggregator +- [#4303](https://github.com/MetaMask/metamask-mobile/pull/4303): [IMPROVEMENT] Add support for env variable for MM_HOMEPAGE +- [#4331](https://github.com/MetaMask/metamask-mobile/pull/4331): [IMPROVEMENT] Fix addressbook and browser test +- [#4170](https://github.com/MetaMask/metamask-mobile/pull/4170): [FIX] Copy to clipboard for Android version 9 and below +- [#4328](https://github.com/MetaMask/metamask-mobile/pull/4328): [FIX] Fix generate-static-assets +- [#4318](https://github.com/MetaMask/metamask-mobile/pull/4318): [FIX] Fix confusables bug +- [#4316](https://github.com/MetaMask/metamask-mobile/pull/4316): [IMPROVEMENT] GIVEN, WHEN, THEN - Template Update +- [#4167](https://github.com/MetaMask/metamask-mobile/pull/4167): [IMPROVEMENT] Adds support for 'dapp/' urls support on 'metamask://' and fixes DL opening to Apple Store +- [#4175](https://github.com/MetaMask/metamask-mobile/pull/4175): [Fix] Favourites not showing when home button is pressed in browser tab menu +- [#4278](https://github.com/MetaMask/metamask-mobile/pull/4278): [IMPROVEMENT] Convert back to spaces +- [#4249](https://github.com/MetaMask/metamask-mobile/pull/4249): [IMPROVEMENT] patch cross-fetch instead of skipping +- [#4174](https://github.com/MetaMask/metamask-mobile/pull/4174): [IMPROVEMENT] Address now is in the checksum standard format +- [#4182](https://github.com/MetaMask/metamask-mobile/pull/4182): [IMPROVEMENT] Standardize prettier configuration +- [#4183](https://github.com/MetaMask/metamask-mobile/pull/4183): [FIX] excluded audit because no available patch ## 5.1.0 - May 5, 2022 - [#3929](https://github.com/MetaMask/metamask-mobile/pull/3929): [IMPROVEMENT] Defaults to current network if chain id not specified in QR codes diff --git a/android/app/build.gradle b/android/app/build.gradle index d4bff01f006..df9c7050835 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -160,8 +160,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 906 - versionName "5.1.1" + versionCode 913 + versionName "5.2.0" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index 88245232c83..949c593eb9f 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -45,6 +45,7 @@ import { allowedToBuy } from '../FiatOrders'; import AssetSwapButton from '../Swaps/components/AssetSwapButton'; import ClipboardManager from '../../../core/ClipboardManager'; import { ThemeContext, mockTheme } from '../../../util/theme'; +import Routes from '../../../constants/navigation/Routes'; const createStyles = (colors) => StyleSheet.create({ @@ -308,13 +309,16 @@ class AccountOverview extends PureComponent { }; onBuy = () => { - this.props.navigation.navigate('FiatOnRampAggregator'); + this.props.navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.ID); InteractionManager.runAfterInteractions(() => { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_OPENED, { - button_location: 'Home Screen', - button_copy: 'Buy', - }); + Analytics.trackEventWithParameters( + AnalyticsV2.ANALYTICS_EVENTS.BUY_BUTTON_CLICKED, + { + text: 'Buy', + location: 'Wallet', + chain_id_destination: this.props.chainId, + }, + ); }); }; diff --git a/app/components/UI/AssetOverview/index.js b/app/components/UI/AssetOverview/index.js index 6e2a7de0206..4f1aa9bc681 100644 --- a/app/components/UI/AssetOverview/index.js +++ b/app/components/UI/AssetOverview/index.js @@ -35,7 +35,6 @@ import Engine from '../../../core/Engine'; import Logger from '../../../util/Logger'; import Analytics from '../../../core/Analytics'; import AnalyticsV2 from '../../../util/analyticsV2'; -import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; import { allowedToBuy } from '../FiatOrders'; import AssetSwapButton from '../Swaps/components/AssetSwapButton'; import NetworkMainAssetLogo from '../NetworkMainAssetLogo'; @@ -182,13 +181,16 @@ class AssetOverview extends PureComponent { }; onBuy = () => { - this.props.navigation.navigate('FiatOnRampAggregator'); + this.props.navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.ID); InteractionManager.runAfterInteractions(() => { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_OPENED, { - button_location: 'Token Screen', - button_copy: 'Buy', - }); + Analytics.trackEventWithParameters( + AnalyticsV2.ANALYTICS_EVENTS.BUY_BUTTON_CLICKED, + { + text: 'Buy', + location: 'Token Screen', + chain_id_destination: this.props.chainId, + }, + ); }); }; diff --git a/app/components/UI/FiatOnRampAggregator/Views/AmountToBuy.tsx b/app/components/UI/FiatOnRampAggregator/Views/AmountToBuy.tsx index 967d70d44f4..6e364b88681 100644 --- a/app/components/UI/FiatOnRampAggregator/Views/AmountToBuy.tsx +++ b/app/components/UI/FiatOnRampAggregator/Views/AmountToBuy.tsx @@ -48,6 +48,7 @@ import ErrorViewWithReporting from '../components/ErrorViewWithReporting'; import { Colors } from '../../../../util/theme/models'; import { CryptoCurrency } from '@consensys/on-ramp-sdk'; import Routes from '../../../../constants/navigation/Routes'; +import useAnalytics from '../hooks/useAnalytics'; // TODO: Convert into typescript and correctly type const Text = BaseText as any; @@ -94,10 +95,11 @@ const AmountToBuy = () => { const navigation = useNavigation(); const { colors } = useTheme(); const styles = createStyles(colors); + const trackEvent = useAnalytics(); const [amountFocused, setAmountFocused] = useState(false); const [amount, setAmount] = useState('0'); const [amountNumber, setAmountNumber] = useState(0); - const [tokens, setTokens] = useState([]); + const [tokens, setTokens] = useState(null); const [error, setError] = useState(null); const keyboardHeight = useRef(1000); const keypadOffset = useSharedValue(1000); @@ -122,16 +124,6 @@ const AmountToBuy = () => { const [isRegionModalVisible, toggleRegionModal, , hideRegionModal] = useModalHandler(false); - useEffect(() => { - navigation.setOptions( - getFiatOnRampAggNavbar( - navigation, - { title: strings('fiat_on_ramp_aggregator.amount_to_buy') }, - colors, - ), - ); - }, [navigation, colors]); - /** * Grab the current state of the SDK via the context. */ @@ -341,6 +333,80 @@ const AmountToBuy = () => { setSelectedPaymentMethodId, ]); + /** + * * Derived values + */ + + const isFetching = + isFetchingSdkCryptoCurrencies || + isFetchingPaymentMethods || + isFetchingFiatCurrencies || + isFetchingDefaultFiatCurrency || + isFetchingCountries; + + /** + * Get the fiat currency object by id + */ + const currentFiatCurrency = useMemo(() => { + const currency = + fiatCurrencies?.find?.((curr) => curr.id === selectedFiatCurrencyId) || + defaultFiatCurrency; + return currency; + }, [fiatCurrencies, defaultFiatCurrency, selectedFiatCurrencyId]); + + const currentPaymentMethod = useMemo( + () => + filteredPaymentMethods?.find?.( + (method) => method.id === selectedPaymentMethodId, + ), + [filteredPaymentMethods, selectedPaymentMethodId], + ); + + /** + * Format the amount for display (iOS only) + */ + const displayAmount = useMemo(() => { + if (Device.isIos() && Intl && Intl?.NumberFormat) { + return amountFocused + ? amount + : new Intl.NumberFormat().format(amountNumber); + } + return amount; + }, [amount, amountFocused, amountNumber]); + + const amountIsBelowMinimum = useMemo( + () => amountNumber !== 0 && limits && amountNumber < limits.minAmount, + [amountNumber, limits], + ); + + const amountIsAboveMaximum = useMemo( + () => amountNumber !== 0 && limits && amountNumber > limits.maxAmount, + [amountNumber, limits], + ); + + const amountIsValid = useMemo( + () => !amountIsBelowMinimum && !amountIsAboveMaximum, + [amountIsBelowMinimum, amountIsAboveMaximum], + ); + + const handleCancelPress = useCallback(() => { + trackEvent('ONRAMP_CANCELED', { + location: 'Amount to Buy Screen', + chain_id_destination: selectedChainId, + }); + }, [selectedChainId, trackEvent]); + + useEffect(() => { + navigation.setOptions( + getFiatOnRampAggNavbar( + navigation, + { title: strings('fiat_on_ramp_aggregator.amount_to_buy') }, + colors, + handleCancelPress, + ), + ); + }, [navigation, colors, handleCancelPress]); + /** * * Keypad style, handlers and effects */ @@ -353,7 +419,7 @@ const AmountToBuy = () => { })); useEffect(() => { - keypadOffset.value = amountFocused ? 40 : keyboardHeight.current + 40; + keypadOffset.value = amountFocused ? 40 : keyboardHeight.current + 80; }, [amountFocused, keyboardHeight, keypadOffset]); /** @@ -488,64 +554,26 @@ const AmountToBuy = () => { navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.GET_QUOTES, { amount: amountNumber, asset: selectedAsset, + fiatCurrency: currentFiatCurrency, }); - }, [amountNumber, navigation, selectedAsset]); - - /** - * * Derived values - */ - - const isFetching = - isFetchingSdkCryptoCurrencies || - isFetchingPaymentMethods || - isFetchingFiatCurrencies || - isFetchingDefaultFiatCurrency || - isFetchingCountries; - - /** - * Get the fiat currency object by id - */ - const currentFiatCurrency = useMemo(() => { - const currency = - fiatCurrencies?.find?.((curr) => curr.id === selectedFiatCurrencyId) || - defaultFiatCurrency; - return currency; - }, [fiatCurrencies, defaultFiatCurrency, selectedFiatCurrencyId]); - - const currentPaymentMethod = useMemo( - () => - filteredPaymentMethods?.find?.( - (method) => method.id === selectedPaymentMethodId, - ), - [filteredPaymentMethods, selectedPaymentMethodId], - ); - - /** - * Format the amount for display (iOS only) - */ - const displayAmount = useMemo(() => { - if (Device.isIos() && Intl && Intl?.NumberFormat) { - return amountFocused - ? amount - : new Intl.NumberFormat().format(amountNumber); - } - return amount; - }, [amount, amountFocused, amountNumber]); - - const amountIsBelowMinimum = useMemo( - () => amountNumber !== 0 && limits && amountNumber < limits.minAmount, - [amountNumber, limits], - ); - - const amountIsAboveMaximum = useMemo( - () => amountNumber !== 0 && limits && amountNumber > limits.maxAmount, - [amountNumber, limits], - ); + trackEvent('ONRAMP_QUOTES_REQUESTED', { + currency_source: currentFiatCurrency?.symbol as string, + currency_destination: selectedAsset?.symbol as string, + payment_method_id: selectedPaymentMethodId as string, + chain_id_destination: selectedChainId, + amount: amountNumber, + location: 'Amount to Buy Screen', + }); + }, [ + amountNumber, + currentFiatCurrency, + navigation, + selectedAsset, + selectedChainId, + selectedPaymentMethodId, + trackEvent, + ]); - const amountIsValid = useMemo( - () => !amountIsBelowMinimum && !amountIsAboveMaximum, - [amountIsBelowMinimum, amountIsAboveMaximum], - ); const retryMethod = useCallback(() => { if (!error) { return null; @@ -650,7 +678,7 @@ const AmountToBuy = () => { ); } - if (!isFetching && (!tokens || tokens.length === 0)) { + if (!isFetching && tokens && tokens.length === 0) { return ( @@ -659,11 +687,21 @@ const AmountToBuy = () => { 'fiat_on_ramp_aggregator.no_tokens_available', { network: NETWORKS_NAMES[selectedChainId], + region: selectedRegion?.name, }, )} - ctaOnPress={() => navigation.goBack()} + ctaLabel={strings('fiat_on_ramp_aggregator.try_different_region')} + ctaOnPress={toggleRegionModal as () => void} /> + void} + onRegionPress={handleRegionPress} + /> ); } @@ -784,7 +822,7 @@ const AmountToBuy = () => { 'fiat_on_ramp_aggregator.select_a_cryptocurrency_description', { network: NETWORKS_NAMES[selectedChainId] }, )} - tokens={tokens} + tokens={tokens ?? []} onItemPress={handleAssetPress} /> { paymentMethods={filteredPaymentMethods} selectedPaymentMethodId={selectedPaymentMethodId} onItemPress={handleChangePaymentMethod} + location={'Amount to Buy Screen'} /> { data={countries} dismiss={hideRegionModal as () => void} onRegionPress={handleRegionPress} + location={'Amount to Buy Screen'} /> ); diff --git a/app/components/UI/FiatOnRampAggregator/Views/Checkout.tsx b/app/components/UI/FiatOnRampAggregator/Views/Checkout.tsx index c26a4bcafa2..fcd8e27b260 100644 --- a/app/components/UI/FiatOnRampAggregator/Views/Checkout.tsx +++ b/app/components/UI/FiatOnRampAggregator/Views/Checkout.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useEffect, useState } from 'react'; import { parseUrl } from 'query-string'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useNavigation, useRoute } from '@react-navigation/native'; import { View } from 'react-native'; import { WebView, WebViewNavigation } from 'react-native-webview'; -import { QuoteResponse, CryptoCurrency } from '@consensys/on-ramp-sdk'; +import { QuoteResponse, CryptoCurrency, Order } from '@consensys/on-ramp-sdk'; import { baseStyles } from '../../../../styles/common'; import { useTheme } from '../../../../util/theme'; import { getFiatOnRampAggNavbar } from '../../Navbar'; @@ -16,36 +16,54 @@ import Engine from '../../../../core/Engine'; import { toLowerCaseEquals } from '../../../../util/general'; import { protectWalletModalVisible } from '../../../../actions/user'; import { - callbackBaseUrl, processAggregatorOrder, aggregatorInitialFiatOrder, } from '../orderProcessor/aggregator'; import NotificationManager from '../../../../core/NotificationManager'; -import { getNotificationDetails } from '../../FiatOrders'; +import { FiatOrder, getNotificationDetails } from '../../FiatOrders'; import ScreenLayout from '../components/ScreenLayout'; import ErrorView from '../components/ErrorView'; import ErrorViewWithReporting from '../components/ErrorViewWithReporting'; import { strings } from '../../../../../locales/i18n'; +import useAnalytics from '../hooks/useAnalytics'; +import { hexToBN } from '../../../../util/number'; const CheckoutWebView = () => { - const { selectedAddress, selectedChainId, sdkError } = useFiatOnRampSDK(); + const { selectedAddress, selectedChainId, sdkError, callbackBaseUrl } = + useFiatOnRampSDK(); const dispatch = useDispatch(); + const trackEvent = useAnalytics(); const [error, setError] = useState(''); const [key, setKey] = useState(0); const navigation = useNavigation(); // @ts-expect-error useRoute params error const { params }: { params: QuoteResponse } = useRoute(); const { colors } = useTheme(); + const accounts = useSelector( + (state: any) => + state.engine.backgroundState.AccountTrackerController.accounts, + ); + const uri = params?.buyURL; + + const handleCancelPress = useCallback(() => { + trackEvent('ONRAMP_CANCELED', { + location: 'Provider Webview', + chain_id_destination: selectedChainId, + provider_onramp: params.provider.name, + }); + }, [params.provider.name, selectedChainId, trackEvent]); + useEffect(() => { navigation.setOptions( getFiatOnRampAggNavbar( navigation, { title: params.provider.name }, colors, + handleCancelPress, ), ); - }, [navigation, colors, params.provider.name]); + }, [navigation, colors, params.provider.name, handleCancelPress]); const addTokenToTokensController = async (token: CryptoCurrency) => { if (!token) return; @@ -86,22 +104,36 @@ const CheckoutWebView = () => { const handleNavigationStateChange = async (navState: WebViewNavigation) => { if (navState?.url.startsWith(callbackBaseUrl)) { try { + const parsedUrl = parseUrl(navState?.url); + if (Object.keys(parsedUrl.query).length === 0) { + // There was no query params in the URL to parse + // Most likely the user clicked the X in Wyre widget + // @ts-expect-error navigation prop mismatch + navigation.dangerouslyGetParent()?.pop(); + return; + } const orders = await SDK.orders(); const orderId = await orders.getOrderIdFromCallback( params?.provider.id, navState?.url, ); + + if (!orderId) { + throw new Error( + `Order ID could not be retrieved. Callback was ${navState?.url}`, + ); + } + const transformedOrder = await processAggregatorOrder( aggregatorInitialFiatOrder({ id: orderId, account: selectedAddress, - network: Number(selectedChainId), + network: selectedChainId, }), ); // add the order to the redux global store handleAddFiatOrder(transformedOrder); // register the token automatically - await addTokenToTokensController( (transformedOrder as any)?.data?.cryptoCurrency, ); @@ -114,14 +146,17 @@ const CheckoutWebView = () => { NotificationManager.showSimpleNotification( getNotificationDetails(transformedOrder as any), ); + trackEvent('ONRAMP_PURCHASE_SUBMITTED', { + provider_onramp: ((transformedOrder as FiatOrder)?.data as Order) + ?.provider?.name, + chain_id_destination: selectedChainId, + is_apple_pay: false, + has_zero_native_balance: accounts[selectedAddress]?.balance + ? (hexToBN(accounts[selectedAddress].balance) as any)?.isZero?.() + : undefined, + }); } catch (navStateError) { - const parsedUrl = parseUrl(navState?.url); - if (Object.keys(parsedUrl.query).length === 0) { - // @ts-expect-error navigation prop mismatch - navigation.dangerouslyGetParent()?.pop(); - } else { - setError((navStateError as Error)?.message); - } + setError((navStateError as Error)?.message); } } }; @@ -160,7 +195,10 @@ const CheckoutWebView = () => { source={{ uri }} onHttpError={(syntheticEvent) => { const { nativeEvent } = syntheticEvent; - if (nativeEvent.url === uri) { + if ( + nativeEvent.url === uri || + nativeEvent.url.startsWith(callbackBaseUrl) + ) { const webviewHttpError = strings( 'fiat_on_ramp_aggregator.webview_received_error', { code: nativeEvent.statusCode }, diff --git a/app/components/UI/FiatOnRampAggregator/Views/GetQuotes.tsx b/app/components/UI/FiatOnRampAggregator/Views/GetQuotes.tsx index 487bd0524fd..0125ee95c00 100644 --- a/app/components/UI/FiatOnRampAggregator/Views/GetQuotes.tsx +++ b/app/components/UI/FiatOnRampAggregator/Views/GetQuotes.tsx @@ -31,13 +31,13 @@ import StyledButton from '../../StyledButton'; import BaseListItem from '../../../Base/ListItem'; import { getFiatOnRampAggNavbar } from '../../Navbar'; -import { callbackBaseUrl } from '../orderProcessor/aggregator'; import useInterval from '../../../hooks/useInterval'; import { strings } from '../../../../../locales/i18n'; import Device from '../../../../util/device'; import { useTheme } from '../../../../util/theme'; import { Colors } from '../../../../util/theme/models'; import { PROVIDER_LINKS } from '../types'; +import useAnalytics from '../hooks/useAnalytics'; // TODO: Convert into typescript and correctly type const Text = BaseText as any; @@ -182,7 +182,9 @@ const GetQuotes = () => { selectedAsset, selectedAddress, selectedFiatCurrencyId, + selectedChainId, appConfig, + callbackBaseUrl, sdkError, } = useFiatOnRampSDK(); @@ -191,6 +193,7 @@ const GetQuotes = () => { const { params } = useRoute(); const navigation = useNavigation(); + const trackEvent = useAnalytics(); const [isLoading, setIsLoading] = useState(true); const [shouldFinishAnimation, setShouldFinishAnimation] = useState(false); const [firstFetchCompleted, setFirstFetchCompleted] = useState(false); @@ -235,11 +238,19 @@ const GetQuotes = () => { callbackBaseUrl, ); - const filteredQuotes = useMemo( + const filteredQuotes: QuoteResponse[] = useMemo( () => (quotes || []).filter(({ error }) => !error).sort(sortByAmountOut), [quotes], ); + const handleCancelPress = useCallback(() => { + trackEvent('ONRAMP_CANCELED', { + location: 'Quotes Screen', + chain_id_destination: selectedChainId, + results_count: filteredQuotes.length, + }); + }, [filteredQuotes.length, selectedChainId, trackEvent]); + // we only activate this interval polling once the first fetch of quotes is successfull useInterval( () => { @@ -300,37 +311,154 @@ const GetQuotes = () => { navigation, { title: strings('fiat_on_ramp_aggregator.select_a_quote') }, colors, + handleCancelPress, ), ); - }, [navigation, colors]); + }, [navigation, colors, handleCancelPress]); useEffect(() => { if (isFetchingQuotes) return; setShouldFinishAnimation(true); }, [isFetchingQuotes]); + useEffect(() => { + if ( + shouldFinishAnimation && + quotes && + !isFetchingQuotes && + pollingCyclesLeft >= 0 + ) { + const quotesWihoutError: QuoteResponse[] = quotes + .filter(({ error }) => !error) + .sort(sortByAmountOut); + const totals = quotesWihoutError.reduce( + (acc, curr) => { + const totalFee = + acc.totalFee + + ((curr?.networkFee || 0) + + (curr?.providerFee || 0) + + (curr?.extraFee || 0)); + return { + amountOut: acc.amountOut + (curr?.amountOut || 0), + totalFee, + totalGasFee: acc.totalGasFee + (curr?.networkFee || 0), + totalProccessingFee: + acc.totalProccessingFee + (curr?.providerFee || 0), + feeAmountRatio: + acc.feeAmountRatio + totalFee / (curr?.amountOut || 0), + }; + }, + { + amountOut: 0, + totalFee: 0, + totalGasFee: 0, + totalProccessingFee: 0, + feeAmountRatio: 0, + }, + ); + trackEvent('ONRAMP_QUOTES_RECEIVED', { + currency_source: (params as any)?.fiatCurrency?.symbol as string, + currency_destination: (params as any)?.asset?.symbol as string, + chain_id_destination: selectedChainId, + amount: (params as any)?.amount as number, + refresh_count: appConfig.POLLING_CYCLES - pollingCyclesLeft, + results_count: filteredQuotes.length, + average_crypto_out: totals.amountOut / filteredQuotes.length, + average_total_fee: totals.totalFee / filteredQuotes.length, + average_gas_fee: totals.totalGasFee / filteredQuotes.length, + average_processing_fee: + totals.totalProccessingFee / filteredQuotes.length, + provider_onramp_list: filteredQuotes.map( + ({ provider }) => provider.name, + ), + provider_onramp_first: filteredQuotes[0]?.provider?.name, + average_total_fee_of_amount: + totals.feeAmountRatio / filteredQuotes.length, + provider_onramp_last: + filteredQuotes.length > 1 + ? filteredQuotes[filteredQuotes.length - 1]?.provider?.name + : undefined, + }); + + if (quotes.length > quotesWihoutError.length) { + trackEvent('ONRAMP_QUOTE_ERROR', { + provider_onramp_list: quotes + .filter(({ error }) => Boolean(error)) + .map(({ provider }) => provider.name), + currency_source: (params as any)?.fiatCurrency?.symbol as string, + currency_destination: (params as any)?.asset?.symbol as string, + chain_id_destination: selectedChainId, + amount: (params as any)?.amount as number, + }); + } + } + }, [ + appConfig.POLLING_CYCLES, + filteredQuotes, + isFetchingQuotes, + params, + pollingCyclesLeft, + quotes, + selectedChainId, + shouldFinishAnimation, + trackEvent, + ]); + useEffect(() => { if (filteredQuotes && filteredQuotes.length > 0) { setProviderId(filteredQuotes[0].provider?.id); } }, [filteredQuotes]); - const handleOnQuotePress = useCallback((quote) => { + const handleOnQuotePress = useCallback((quote: QuoteResponse) => { setProviderId(quote.provider.id); }, []); - const handleInfoPress = useCallback((quote) => { - if (quote?.provider) { - setSelectedProviderInfo(quote.provider); - setShowProviderInfo(true); - } - }, []); + const handleInfoPress = useCallback( + (quote) => { + if (quote?.provider) { + setSelectedProviderInfo(quote.provider); + setShowProviderInfo(true); + trackEvent('ONRAMP_PROVIDER_DETAILS_VIEWED', { + provider_onramp: quote.provider.name, + }); + } + }, + [trackEvent], + ); const handleOnPressBuy = useCallback( - (quote) => { + (quote, index) => { quote?.provider?.id && navigation.navigate('Checkout', { ...quote }); + const totalFee = + (quote.networkFee || 0) + + (quote.providerFee || 0) + + (quote.extraFee || 0); + trackEvent('ONRAMP_PROVIDER_SELECTED', { + provider_onramp: quote.provider.name, + refresh_count: appConfig.POLLING_CYCLES - pollingCyclesLeft, + quote_position: index + 1, + results_count: filteredQuotes.length, + crypto_out: quote.amountOut || 0, + currency_source: (params as any)?.fiatCurrency?.symbol as string, + currency_destination: (params as any)?.asset?.symbol as string, + chain_id_destination: selectedChainId, + total_fee: totalFee, + gas_fee: quote.networkFee || 0, + processing_fee: quote.providerFee || 0, + exchange_rate: + ((quote.amountIn || 0) - totalFee) / (quote.amountOut || 0), + }); }, - [navigation], + [ + appConfig.POLLING_CYCLES, + filteredQuotes.length, + navigation, + params, + pollingCyclesLeft, + selectedChainId, + trackEvent, + ], ); const handleFetchQuotes = useCallback(() => { @@ -340,7 +468,23 @@ const GetQuotes = () => { setPollingCyclesLeft(appConfig.POLLING_CYCLES - 1); setRemainingTime(appConfig.POLLING_INTERVAL); fetchQuotes(); - }, [appConfig.POLLING_CYCLES, appConfig.POLLING_INTERVAL, fetchQuotes]); + trackEvent('ONRAMP_QUOTES_REQUESTED', { + currency_source: (params as any)?.fiatCurrency?.symbol as string, + currency_destination: (params as any)?.asset?.symbol as string, + payment_method_id: selectedPaymentMethodId as string, + chain_id_destination: selectedChainId, + amount: (params as any)?.amount as number, + location: 'Quotes Screen', + }); + }, [ + appConfig.POLLING_CYCLES, + appConfig.POLLING_INTERVAL, + fetchQuotes, + params, + selectedChainId, + selectedPaymentMethodId, + trackEvent, + ]); const QuotesPolling = () => ( @@ -502,7 +646,7 @@ const GetQuotes = () => { handleOnQuotePress(quote)} - onPressBuy={() => handleOnPressBuy(quote)} + onPressBuy={() => handleOnPressBuy(quote, index)} highlighted={quote.provider.id === providerId} showInfo={() => handleInfoPress(quote)} /> diff --git a/app/components/UI/FiatOnRampAggregator/Views/GetStarted.tsx b/app/components/UI/FiatOnRampAggregator/Views/GetStarted.tsx index 2949470e321..e9b0571a6c2 100644 --- a/app/components/UI/FiatOnRampAggregator/Views/GetStarted.tsx +++ b/app/components/UI/FiatOnRampAggregator/Views/GetStarted.tsx @@ -10,6 +10,7 @@ import { useTheme } from '../../../../util/theme'; import { useFiatOnRampSDK } from '../sdk'; import ErrorViewWithReportingJS from '../components/ErrorViewWithReporting'; import Routes from '../../../../constants/navigation/Routes'; +import useAnalytics from '../hooks/useAnalytics'; /* eslint-disable import/no-commonjs, @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ const getStartedIcon = require('../components/images/WalletInfo.png'); @@ -53,10 +54,19 @@ const styles = StyleSheet.create({ const GetStarted: React.FC = () => { const navigation = useNavigation(); - const { getStarted, setGetStarted, sdkError } = useFiatOnRampSDK(); + const { getStarted, setGetStarted, sdkError, selectedChainId } = + useFiatOnRampSDK(); + const trackEvent = useAnalytics(); const { colors } = useTheme(); + const handleCancelPress = useCallback(() => { + trackEvent('ONRAMP_CANCELED', { + location: 'Get Started Screen', + chain_id_destination: selectedChainId, + }); + }, [selectedChainId, trackEvent]); + useEffect(() => { navigation.setOptions( getFiatOnRampAggNavbar( @@ -66,9 +76,10 @@ const GetStarted: React.FC = () => { showBack: false, }, colors, + handleCancelPress, ), ); - }, [navigation, colors]); + }, [navigation, colors, handleCancelPress]); const handleOnPress = useCallback(() => { navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.REGION); diff --git a/app/components/UI/FiatOnRampAggregator/Views/OrderDetails.tsx b/app/components/UI/FiatOnRampAggregator/Views/OrderDetails.tsx index 4dffe1db325..c060ba8e233 100644 --- a/app/components/UI/FiatOnRampAggregator/Views/OrderDetails.tsx +++ b/app/components/UI/FiatOnRampAggregator/Views/OrderDetails.tsx @@ -3,7 +3,7 @@ import { View, StyleSheet } from 'react-native'; import ScreenLayout from '../components/ScreenLayout'; import StyledButton from '../../StyledButton'; import { useNavigation, useRoute } from '@react-navigation/native'; -import TransactionDetail from '../components/TransactionDetails'; +import OrderDetail from '../components/OrderDetails'; import Account from '../components/Account'; import { strings } from '../../../../../locales/i18n'; import { makeOrderIdSelector } from '../../../../reducers/fiatOrders'; @@ -12,6 +12,9 @@ import { getFiatOnRampAggNavbar } from '../../Navbar'; import { useTheme } from '../../../../util/theme'; import { ScrollView } from 'react-native-gesture-handler'; import Routes from '../../../../constants/navigation/Routes'; +import { FiatOrder } from '../../FiatOrders'; +import useAnalytics from '../hooks/useAnalytics'; +import { Order } from '@consensys/on-ramp-sdk'; const styles = StyleSheet.create({ screenLayout: { @@ -20,6 +23,7 @@ const styles = StyleSheet.create({ }); const OrderDetails = () => { + const trackEvent = useAnalytics(); const provider = useSelector( (state: any) => state.engine.backgroundState.NetworkController.provider, ); @@ -28,8 +32,10 @@ const OrderDetails = () => { state.engine.backgroundState.PreferencesController.frequentRpcList, ); const routes = useRoute(); - // @ts-expect-error expect params error - const order = useSelector(makeOrderIdSelector(routes?.params?.orderId)); + const order: FiatOrder = useSelector( + // @ts-expect-error expect params error + makeOrderIdSelector(routes?.params?.orderId), + ); const { colors } = useTheme(); const navigation = useNavigation(); @@ -38,7 +44,7 @@ const OrderDetails = () => { getFiatOnRampAggNavbar( navigation, { - title: strings('fiat_on_ramp_aggregator.transaction.details_main'), + title: strings('fiat_on_ramp_aggregator.order_details.details_main'), showBack: false, }, colors, @@ -46,6 +52,19 @@ const OrderDetails = () => { ); }, [colors, navigation]); + useEffect(() => { + if (order) { + trackEvent('ONRAMP_PURCHASE_DETAILS_VIEWED', { + purchase_status: order.state, + provider_onramp: (order.data as Order)?.provider.name, + currency_destination: order.cryptocurrency, + currency_source: order.currency, + chain_id_destination: order.network, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [trackEvent]); + const handleMakeAnotherPurchase = useCallback(() => { navigation.goBack(); navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.ID); @@ -63,7 +82,7 @@ const OrderDetails = () => { - { {strings( - 'fiat_on_ramp_aggregator.transaction.another_purchase', + 'fiat_on_ramp_aggregator.order_details.another_purchase', )} diff --git a/app/components/UI/FiatOnRampAggregator/Views/PaymentMethod.tsx b/app/components/UI/FiatOnRampAggregator/Views/PaymentMethod.tsx index e27d7f4a6c1..afe317b8194 100644 --- a/app/components/UI/FiatOnRampAggregator/Views/PaymentMethod.tsx +++ b/app/components/UI/FiatOnRampAggregator/Views/PaymentMethod.tsx @@ -18,6 +18,7 @@ import Box from '../components/Box'; import ErrorView from '../components/ErrorView'; import ErrorViewWithReporting from '../components/ErrorViewWithReporting'; import Routes from '../../../../constants/navigation/Routes'; +import useAnalytics from '../hooks/useAnalytics'; // TODO: Convert into typescript and correctly type const Text = BaseText as any; @@ -52,25 +53,13 @@ const SkeletonPaymentOption = () => ( const PaymentMethod = () => { const navigation = useNavigation(); const { colors } = useTheme(); - - useEffect(() => { - navigation.setOptions( - getFiatOnRampAggNavbar( - navigation, - { - title: strings( - 'fiat_on_ramp_aggregator.payment_method.payment_method', - ), - }, - colors, - ), - ); - }, [navigation, colors]); + const trackEvent = useAnalytics(); const { selectedRegion, selectedPaymentMethodId, setSelectedPaymentMethodId, + selectedChainId, sdkError, } = useFiatOnRampSDK(); @@ -103,10 +92,43 @@ const PaymentMethod = () => { setSelectedPaymentMethodId, ]); + const handleCancelPress = useCallback(() => { + trackEvent('ONRAMP_CANCELED', { + location: 'Payment Method Screen', + chain_id_destination: selectedChainId, + }); + }, [selectedChainId, trackEvent]); + + const handlePaymentMethodPress = useCallback( + (id) => { + setSelectedPaymentMethodId(id); + trackEvent('ONRAMP_PAYMENT_METHOD_SELECTED', { + payment_method_id: id, + location: 'Payment Method Screen', + }); + }, + [setSelectedPaymentMethodId, trackEvent], + ); + const handleContinueToAmount = useCallback(() => { navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.AMOUNT_TO_BUY); }, [navigation]); + useEffect(() => { + navigation.setOptions( + getFiatOnRampAggNavbar( + navigation, + { + title: strings( + 'fiat_on_ramp_aggregator.payment_method.payment_method', + ), + }, + colors, + handleCancelPress, + ), + ); + }, [navigation, colors, handleCancelPress]); + if (sdkError) { return ( @@ -155,7 +177,7 @@ const PaymentMethod = () => { onPress={ id === selectedPaymentMethodId ? undefined - : () => setSelectedPaymentMethodId(id) + : () => handlePaymentMethodPress(id) } amountTier={amountTier} paymentType={getPaymentMethodIcon(id)} diff --git a/app/components/UI/FiatOnRampAggregator/Views/Region.tsx b/app/components/UI/FiatOnRampAggregator/Views/Region.tsx index 674713a4fc6..5b352f880f6 100644 --- a/app/components/UI/FiatOnRampAggregator/Views/Region.tsx +++ b/app/components/UI/FiatOnRampAggregator/Views/Region.tsx @@ -19,6 +19,7 @@ import ErrorView from '../components/ErrorView'; import ErrorViewWithReporting from '../components/ErrorViewWithReporting'; import { Region } from '../types'; import Routes from '../../../../constants/navigation/Routes'; +import useAnalytics from '../hooks/useAnalytics'; // TODO: Convert into typescript and correctly type const Text = BaseText as any; @@ -33,11 +34,13 @@ const styles = StyleSheet.create({ const RegionView = () => { const navigation = useNavigation(); const { colors } = useTheme(); + const trackEvent = useAnalytics(); const { selectedRegion, setSelectedRegion, setSelectedFiatCurrencyId, sdkError, + selectedChainId, } = useFiatOnRampSDK(); const [isRegionModalVisible, , showRegionModal, hideRegionModal] = useModalHandler(false); @@ -48,6 +51,13 @@ const RegionView = () => { const [{ data, isFetching, error }, queryGetCountries] = useSDKMethod('getCountries'); + const handleCancelPress = useCallback(() => { + trackEvent('ONRAMP_CANCELED', { + location: 'Region Screen', + chain_id_destination: selectedChainId, + }); + }, [selectedChainId, trackEvent]); + useEffect(() => { navigation.setOptions( getFiatOnRampAggNavbar( @@ -57,9 +67,10 @@ const RegionView = () => { showBack: false, }, colors, + handleCancelPress, ), ); - }, [navigation, colors]); + }, [navigation, colors, handleCancelPress]); const handleOnPress = useCallback(() => { navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.PAYMENT_METHOD); @@ -183,6 +194,7 @@ const RegionView = () => { data={data} dismiss={hideRegionModal as () => void} onRegionPress={handleRegionPress} + location={'Region Screen'} /> diff --git a/app/components/UI/FiatOnRampAggregator/components/ErrorViewWithReporting.tsx b/app/components/UI/FiatOnRampAggregator/components/ErrorViewWithReporting.tsx index 0721507b6aa..a2a1382bfc9 100644 --- a/app/components/UI/FiatOnRampAggregator/components/ErrorViewWithReporting.tsx +++ b/app/components/UI/FiatOnRampAggregator/components/ErrorViewWithReporting.tsx @@ -2,7 +2,6 @@ import React from 'react'; import ErrorView from './ErrorView'; import { useNavigation } from '@react-navigation/native'; import { strings } from '../../../../../locales/i18n'; - /** * ErrorViewWithReporting is a functional general-purpose UI component responsible to show error details in Fiat On-Ramp * @@ -11,6 +10,7 @@ import { strings } from '../../../../../locales/i18n'; */ function ErrorViewWithReporting({ error }: { error: Error }) { const navigation = useNavigation(); + return ( { //TODO: implement a reporting mechanisim for the sdkError + //TODO: implement a mechanisim for user to submit a support ticket // @ts-expect-error navigation prop mismatch navigation.dangerouslyGetParent()?.pop(); }} diff --git a/app/components/UI/FiatOnRampAggregator/components/InfoAlert.tsx b/app/components/UI/FiatOnRampAggregator/components/InfoAlert.tsx index 751c6b56757..9705786ebe0 100644 --- a/app/components/UI/FiatOnRampAggregator/components/InfoAlert.tsx +++ b/app/components/UI/FiatOnRampAggregator/components/InfoAlert.tsx @@ -11,6 +11,7 @@ import RemoteImage from '../../../Base/RemoteImage'; import { useAssetFromTheme, useTheme } from '../../../../util/theme'; import { strings } from '../../../../../locales/i18n'; import { Colors } from '../../../../util/theme/models'; +import useAnalytics from '../hooks/useAnalytics'; type Logos = QuoteResponse['provider']['logos']; @@ -70,15 +71,45 @@ const InfoAlert: React.FC = ({ }: Props) => { const { colors } = useTheme(); const styles = createStyles(colors); + const trackEvent = useAnalytics(); const logoKey: 'light' | 'dark' = useAssetFromTheme('light', 'dark'); - const handleLinkPress = useCallback(async (url: string) => { - const supported = await Linking.canOpenURL(url); - if (supported) { - await Linking.openURL(url); - } - }, []); + const handleProviderHomepageLinkPress = useCallback( + (url: string) => { + Linking.openURL(url); + trackEvent('ONRAMP_EXTERNAL_LINK_CLICKED', { + location: 'Quotes Screen', + text: 'Provider Homepage', + url_domain: url, + }); + }, + [trackEvent], + ); + + const handleProviderPrivacyPolicyLinkPress = useCallback( + (url: string) => { + Linking.openURL(url); + trackEvent('ONRAMP_EXTERNAL_LINK_CLICKED', { + location: 'Quotes Screen', + text: 'Provider Privacy Policy', + url_domain: url, + }); + }, + [trackEvent], + ); + + const handleProviderSupportLinkPress = useCallback( + (url: string) => { + Linking.openURL(url); + trackEvent('ONRAMP_EXTERNAL_LINK_CLICKED', { + location: 'Quotes Screen', + text: 'Provider Support', + url_domain: url, + }); + }, + [trackEvent], + ); return ( = ({ {Boolean(body) && {body}} {Boolean(providerWebsite) && ( handleLinkPress(providerWebsite as string)} + onPress={() => + handleProviderHomepageLinkPress(providerWebsite as string) + } > {providerWebsite} @@ -130,7 +163,11 @@ const InfoAlert: React.FC = ({ )} {Boolean(providerPrivacyPolicy) && ( handleLinkPress(providerPrivacyPolicy as string)} + onPress={() => + handleProviderPrivacyPolicyLinkPress( + providerPrivacyPolicy as string, + ) + } > {strings('app_information.privacy_policy')} @@ -139,12 +176,14 @@ const InfoAlert: React.FC = ({ )} {Boolean(providerSupport) && ( handleLinkPress(providerSupport as string)} + onPress={() => + handleProviderSupportLinkPress(providerSupport as string) + } > {providerName + ' ' + - strings('fiat_on_ramp_aggregator.transaction.support')} + strings('fiat_on_ramp_aggregator.order_details.support')} )} diff --git a/app/components/UI/FiatOnRampAggregator/components/TransactionDetails.tsx b/app/components/UI/FiatOnRampAggregator/components/OrderDetails.tsx similarity index 64% rename from app/components/UI/FiatOnRampAggregator/components/TransactionDetails.tsx rename to app/components/UI/FiatOnRampAggregator/components/OrderDetails.tsx index 82b1527131b..52888ad0b22 100644 --- a/app/components/UI/FiatOnRampAggregator/components/TransactionDetails.tsx +++ b/app/components/UI/FiatOnRampAggregator/components/OrderDetails.tsx @@ -7,7 +7,7 @@ import { Linking, } from 'react-native'; import Feather from 'react-native-vector-icons/Feather'; -import { OrderStatusEnum } from '@consensys/on-ramp-sdk'; +import { Order, OrderStatusEnum } from '@consensys/on-ramp-sdk'; import Box from './Box'; import CustomText from '../../../Base/Text'; import BaseListItem from '../../../Base/ListItem'; @@ -22,6 +22,9 @@ import { import { getProviderName } from '../../../../reducers/fiatOrders'; import useBlockExplorer from '../../Swaps/utils/useBlockExplorer'; import Spinner from '../../AnimatedSpinner'; +import useAnalytics from '../hooks/useAnalytics'; +import { FiatOrder } from '../../FiatOrders'; +import { PROVIDER_LINKS } from '../types'; /* eslint-disable import/no-commonjs, @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ const failedIcon = require('./images/TransactionIcon_Failed.png'); // TODO: Convert into typescript and correctly type optionals @@ -80,6 +83,9 @@ const createStyles = (colors: any) => alignSelf: 'center', paddingTop: 15, }, + flexZero: { + flex: 0, + }, }); interface PropsStage { @@ -107,14 +113,14 @@ const Stage: React.FC = ({ color={colors.success.default} /> - {strings('fiat_on_ramp_aggregator.transaction.successful')} + {strings('fiat_on_ramp_aggregator.order_details.successful')} - {strings('fiat_on_ramp_aggregator.transaction.your')}{' '} + {strings('fiat_on_ramp_aggregator.order_details.your')}{' '} {cryptocurrency || - strings('fiat_on_ramp_aggregator.transaction.crypto')}{' '} + strings('fiat_on_ramp_aggregator.order_details.crypto')}{' '} {strings( - 'fiat_on_ramp_aggregator.transaction.available_in_account', + 'fiat_on_ramp_aggregator.order_details.available_in_account', )} @@ -127,15 +133,18 @@ const Stage: React.FC = ({ {stage === 'FAILED' - ? strings('fiat_on_ramp_aggregator.transaction.failed') + ? strings('fiat_on_ramp_aggregator.order_details.failed') : 'fiat_on_ramp.cancelled'} - {strings('fiat_on_ramp_aggregator.transaction.failed_description', { - provider: - providerName || - strings('fiat_on_ramp_aggregator.transaction.the_provider'), - })} + {strings( + 'fiat_on_ramp_aggregator.order_details.failed_description', + { + provider: + providerName || + strings('fiat_on_ramp_aggregator.order_details.the_provider'), + }, + )} ); @@ -148,19 +157,19 @@ const Stage: React.FC = ({ {stage === 'PENDING' - ? strings('fiat_on_ramp_aggregator.transaction.processing') + ? strings('fiat_on_ramp_aggregator.order_details.processing') : strings('transaction.submitted')} {!paymentType?.includes('Credit') ? ( {strings( - 'fiat_on_ramp_aggregator.transaction.processing_bank_description', + 'fiat_on_ramp_aggregator.order_details.processing_bank_description', )} ) : ( {strings( - 'fiat_on_ramp_aggregator.transaction.processing_card_description', + 'fiat_on_ramp_aggregator.order_details.processing_card_description', )} )} @@ -174,7 +183,7 @@ interface Props { /** * Object that represents the current route info like params passed to it */ - order: any; + order: FiatOrder; /** * Current Network provider */ @@ -185,7 +194,7 @@ interface Props { frequentRpcList: any; } -const TransactionDetails: React.FC = ({ +const OrderDetails: React.FC = ({ order, provider, frequentRpcList, @@ -203,194 +212,220 @@ const TransactionDetails: React.FC = ({ cryptocurrency, } = order; const { colors } = useTheme(); + const trackEvent = useAnalytics(); const explorer = useBlockExplorer(provider, frequentRpcList); const styles = createStyles(colors); const date = toDateFormat(createdAt); const amountOut = Number(amount) - Number(cryptoFee); const exchangeRate = Number(amountOut) / Number(cryptoAmount); const providerName = getProviderName(order.provider, data); - const handleLinkPress = useCallback(async (url: string) => { - const supported = await Linking.canOpenURL(url); - if (supported) { - await Linking.openURL(url); - } - }, []); + + const handleExplorerLinkPress = useCallback( + (url: string) => { + Linking.openURL(url); + trackEvent('ONRAMP_EXTERNAL_LINK_CLICKED', { + location: 'Order Details Screen', + text: 'Etherscan Transaction', + url_domain: url, + }); + }, + [trackEvent], + ); + + const handleProviderLinkPress = useCallback( + (url: string) => { + Linking.openURL(url); + trackEvent('ONRAMP_EXTERNAL_LINK_CLICKED', { + location: 'Order Details Screen', + text: 'Provider Order Tracking', + url_domain: url, + }); + }, + [trackEvent], + ); + + const orderData = data as Order; + + const supportLinkUrl = orderData?.provider?.links?.find( + (link) => link.name === PROVIDER_LINKS.SUPPORT, + )?.url; + return ( - {data?.cryptoCurrency?.decimals && + {orderData?.cryptoCurrency?.decimals && cryptoAmount && cryptoAmount !== 0 && cryptocurrency ? ( renderFromTokenMinimalUnit( toTokenMinimalUnit( cryptoAmount, - data?.cryptoCurrency?.decimals, + orderData?.cryptoCurrency?.decimals, ).toString(), - data?.cryptoCurrency?.decimals, + orderData?.cryptoCurrency?.decimals, ) ) : ( ... )}{' '} {cryptocurrency} - {data?.fiatCurrency?.decimals && currencySymbol ? ( + {orderData?.fiatCurrency?.decimals && currencySymbol ? ( {currencySymbol} - {renderFiat(amountOut, currency, data?.fiatCurrency?.decimals)} + {renderFiat(amountOut, currency, orderData?.fiatCurrency?.decimals)} ) : ( ... )} - {strings('fiat_on_ramp_aggregator.transaction.details')} + {strings('fiat_on_ramp_aggregator.order_details.details')} - {strings('fiat_on_ramp_aggregator.transaction.id')} + {strings('fiat_on_ramp_aggregator.order_details.id')} - - - {data?.providerOrderId} + + + {orderData?.providerOrderId} - + - {strings('fiat_on_ramp_aggregator.transaction.date_and_time')} + {strings('fiat_on_ramp_aggregator.order_details.date_and_time')} - + {date} - + - {data?.paymentMethod?.name && ( + {orderData?.paymentMethod?.name && ( {strings( - 'fiat_on_ramp_aggregator.transaction.payment_method', + 'fiat_on_ramp_aggregator.order_details.payment_method', )} - + - {data?.paymentMethod?.name} + {orderData?.paymentMethod?.name} - + )} - {order.provider && data?.paymentMethod?.name && ( + {order.provider && orderData?.paymentMethod?.name && ( - {strings('fiat_on_ramp_aggregator.transaction.via')}{' '} + {strings('fiat_on_ramp_aggregator.order_details.via')}{' '} {providerName} )} - {strings('fiat_on_ramp_aggregator.transaction.token_amount')} + {strings('fiat_on_ramp_aggregator.order_details.token_amount')} - - {cryptoAmount && data?.cryptoCurrency?.decimals ? ( + + {cryptoAmount && orderData?.cryptoCurrency?.decimals ? ( {renderFromTokenMinimalUnit( toTokenMinimalUnit( cryptoAmount, - data?.cryptoCurrency?.decimals, + orderData?.cryptoCurrency?.decimals, ).toString(), - data?.cryptoCurrency?.decimals, + orderData?.cryptoCurrency?.decimals, )}{' '} {cryptocurrency} ) : ( ... )} - + - {strings('fiat_on_ramp_aggregator.transaction.exchange_rate')} + {strings('fiat_on_ramp_aggregator.order_details.exchange_rate')} - + {order.cryptocurrency && isFinite(exchangeRate) && currency && - data?.fiatCurrency?.decimals ? ( + orderData?.fiatCurrency?.decimals ? ( 1 {order.cryptocurrency} @{' '} {renderFiat( exchangeRate, currency, - data?.fiatCurrency?.decimals, + orderData?.fiatCurrency?.decimals, )} ) : ( ... )} - + {currency}{' '} - {strings('fiat_on_ramp_aggregator.transaction.amount')} + {strings('fiat_on_ramp_aggregator.order_details.amount')} - - {data?.fiatCurrency?.decimals && amountOut && currency ? ( + + {orderData?.fiatCurrency?.decimals && amountOut && currency ? ( {currencySymbol} {renderFiat( amountOut, currency, - data?.fiatCurrency?.decimals, + orderData?.fiatCurrency?.decimals, )} ) : ( ... )} - + - {strings('fiat_on_ramp_aggregator.transaction.total_fees')} + {strings('fiat_on_ramp_aggregator.order_details.total_fees')} - - {cryptoFee && currency && data?.fiatCurrency?.decimals ? ( + + {cryptoFee && currency && orderData?.fiatCurrency?.decimals ? ( {currencySymbol} {renderFiat( - cryptoFee, + cryptoFee as number, currency, - data?.fiatCurrency?.decimals, + orderData?.fiatCurrency?.decimals, )} ) : ( ... )} - + @@ -399,49 +434,55 @@ const TransactionDetails: React.FC = ({ - {strings('fiat_on_ramp_aggregator.transaction.purchase_amount')} + {strings('fiat_on_ramp_aggregator.order_details.purchase_amount')} - + {currencySymbol && amount && currency && - data?.fiatCurrency?.decimals ? ( + orderData?.fiatCurrency?.decimals ? ( {currencySymbol} - {renderFiat(amount, currency, data?.fiatCurrency?.decimals)} + {renderFiat( + amount as number, + currency, + orderData?.fiatCurrency?.decimals, + )} ) : ( ... )} - + {order.state === OrderStatusEnum.Completed && txHash && ( handleLinkPress(explorer.tx(txHash))} + onPress={() => handleExplorerLinkPress(explorer.tx(txHash))} > - {strings('fiat_on_ramp_aggregator.transaction.etherscan')}{' '} + {strings('fiat_on_ramp_aggregator.order_details.etherscan')}{' '} {explorer.isValid ? explorer.name : strings( - 'fiat_on_ramp_aggregator.transaction.a_block_explorer', + 'fiat_on_ramp_aggregator.order_details.a_block_explorer', )} )} - {data?.providerLink && ( + {Boolean(supportLinkUrl) && ( - {strings('fiat_on_ramp_aggregator.transaction.questions')}{' '} + {strings('fiat_on_ramp_aggregator.order_details.questions')}{' '} - handleLinkPress(data?.providerLink)}> + handleProviderLinkPress(supportLinkUrl as string)} + > {order.provider && data && ( - {strings('fiat_on_ramp_aggregator.transaction.contact')}{' '} + {strings('fiat_on_ramp_aggregator.order_details.contact')}{' '} {providerName}{' '} - {strings('fiat_on_ramp_aggregator.transaction.support')} + {strings('fiat_on_ramp_aggregator.order_details.support')} )} @@ -451,4 +492,4 @@ const TransactionDetails: React.FC = ({ ); }; -export default TransactionDetails; +export default OrderDetails; diff --git a/app/components/UI/FiatOnRampAggregator/components/PaymentMethodModal.tsx b/app/components/UI/FiatOnRampAggregator/components/PaymentMethodModal.tsx index 744a67f22a9..98c5cc4aa12 100644 --- a/app/components/UI/FiatOnRampAggregator/components/PaymentMethodModal.tsx +++ b/app/components/UI/FiatOnRampAggregator/components/PaymentMethodModal.tsx @@ -1,17 +1,19 @@ import React, { useCallback } from 'react'; import { StyleSheet, SafeAreaView, View, ScrollView } from 'react-native'; import Modal from 'react-native-modal'; +import { Payment } from '@consensys/on-ramp-sdk'; +import BaseText from '../../../Base/Text'; import ScreenLayout from './ScreenLayout'; import ModalDragger from '../../../Base/ModalDragger'; import PaymentOption from './PaymentOption'; -import { useTheme } from '../../../../util/theme'; +import useAnalytics from '../hooks/useAnalytics'; import { getPaymentMethodIcon } from '../utils'; -import BaseText from '../../../Base/Text'; -import { strings } from '../../../../../locales/i18n'; +import { useTheme } from '../../../../util/theme'; import { Colors } from '../../../../util/theme/models'; -import { Payment } from '@consensys/on-ramp-sdk'; +import { ScreenLocation } from '../types'; +import { strings } from '../../../../../locales/i18n'; // TODO: Convert into typescript and correctly type const Text = BaseText as any; @@ -47,6 +49,7 @@ interface Props { onItemPress: (paymentMethodId?: Payment['id']) => void; paymentMethods?: Payment[] | null; selectedPaymentMethodId: Payment['id'] | null; + location?: ScreenLocation; } function PaymentMethodModal({ @@ -56,19 +59,25 @@ function PaymentMethodModal({ onItemPress, paymentMethods, selectedPaymentMethodId, + location, }: Props) { const { colors } = useTheme(); const styles = createStyles(colors); + const trackEvent = useAnalytics(); const handleOnPressItemCallback = useCallback( (paymentMethodId) => { if (selectedPaymentMethodId !== paymentMethodId) { onItemPress(paymentMethodId); + trackEvent('ONRAMP_PAYMENT_METHOD_SELECTED', { + payment_method_id: paymentMethodId, + location, + }); } else { onItemPress(); } }, - [onItemPress, selectedPaymentMethodId], + [location, onItemPress, selectedPaymentMethodId, trackEvent], ); return ( diff --git a/app/components/UI/FiatOnRampAggregator/components/RegionAlert.tsx b/app/components/UI/FiatOnRampAggregator/components/RegionAlert.tsx index 1c3dc1fc9a4..b26f0c79ee5 100644 --- a/app/components/UI/FiatOnRampAggregator/components/RegionAlert.tsx +++ b/app/components/UI/FiatOnRampAggregator/components/RegionAlert.tsx @@ -57,12 +57,10 @@ const RegionAlert: React.FC = ({ const { colors } = useTheme(); const styles = createStyles(colors); - const handleSupportLinkPress = useCallback(async () => { - const supported = await Linking.canOpenURL(SUPPORT_URL); - if (supported) { - await Linking.openURL(SUPPORT_URL); - } - }, []); + const handleSupportLinkPress = useCallback( + () => Linking.openURL(SUPPORT_URL), + [], + ); return ( any; data?: Region[] | null; onRegionPress: (region: Region) => any; + location?: ScreenLocation; } const RegionModal: React.FC = ({ @@ -129,8 +131,10 @@ const RegionModal: React.FC = ({ data, onRegionPress, dismiss, + location, }: Props) => { const { colors } = useTheme(); + const trackEvent = useAnalytics(); const styles = createStyles(colors); const searchInput = useRef(null); const list = useRef>(null); @@ -176,14 +180,22 @@ const RegionModal: React.FC = ({ setRegionInTransit(region); setCurrentData(region.states as Region[]); setSearchString(''); - } else if (region.unsupported) { + return; + } + if (region.unsupported) { setUnsupportedRegion(region); setShowAlert(true); } else { onRegionPress(region); } + trackEvent('ONRAMP_REGION_SELECTED', { + is_supported: region.unsupported, + country_onramp_id: regionInTransit?.id ?? region.id, + state_onramp_id: regionInTransit ? region.id : undefined, + location, + }); }, - [onRegionPress], + [location, onRegionPress, regionInTransit, trackEvent], ); const renderRegionItem = useCallback( diff --git a/app/components/UI/FiatOnRampAggregator/containers/ApplePayButton.tsx b/app/components/UI/FiatOnRampAggregator/containers/ApplePayButton.tsx index e4efa1253d1..b873a05ca68 100644 --- a/app/components/UI/FiatOnRampAggregator/containers/ApplePayButton.tsx +++ b/app/components/UI/FiatOnRampAggregator/containers/ApplePayButton.tsx @@ -1,17 +1,19 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; +import { Order, QuoteResponse } from '@consensys/on-ramp-sdk'; import { protectWalletModalNotVisible } from '../../../../actions/user'; import { addFiatOrder } from '../../../../reducers/fiatOrders'; import ApplePayButtonComponent from '../components/ApplePayButton'; import useApplePay, { ABORTED } from '../hooks/applePay'; +import useAnalytics from '../hooks/useAnalytics'; import Logger from '../../../../util/Logger'; -import NotificationManager from '../../../../core/NotificationManager'; import { strings } from '../../../../../locales/i18n'; import { setLockTime } from '../../../../actions/settings'; -import { FiatOrder, getNotificationDetails } from '../../FiatOrders'; -import { Order, QuoteResponse } from '@consensys/on-ramp-sdk'; import { aggregatorOrderToFiatOrder } from '../orderProcessor/aggregator'; +import { FiatOrder, getNotificationDetails } from '../../FiatOrders'; +import NotificationManager from '../../../../core/NotificationManager'; +import { hexToBN } from '../../../../util/number'; const ApplePayButton = ({ quote, @@ -22,8 +24,19 @@ const ApplePayButton = ({ }) => { const navigation = useNavigation(); const dispatch = useDispatch(); - const network = useSelector( - (state: any) => state.engine.backgroundState.NetworkController.network, + const trackEvent = useAnalytics(); + const accounts = useSelector( + (state: any) => + state.engine.backgroundState.AccountTrackerController.accounts, + ); + + const selectedAddress = useSelector( + (state: any) => + state.engine.backgroundState.PreferencesController.selectedAddress, + ); + const chainId = useSelector( + (state: any) => + state.engine.backgroundState.NetworkController.provider.chainId, ); const [pay] = useApplePay(quote) as [() => Promise]; const lockTime = useSelector((state: any) => state.settings.lockTime); @@ -46,7 +59,7 @@ const ApplePayButton = ({ if (order) { const fiatOrder: FiatOrder = { ...aggregatorOrderToFiatOrder(order), - network, + network: chainId, }; addOrder(fiatOrder); // @ts-expect-error pop is not defined @@ -55,6 +68,14 @@ const ApplePayButton = ({ NotificationManager.showSimpleNotification( getNotificationDetails(fiatOrder), ); + trackEvent('ONRAMP_PURCHASE_SUBMITTED', { + provider_onramp: (fiatOrder?.data as Order)?.provider?.name, + chain_id_destination: chainId, + is_apple_pay: true, + has_zero_native_balance: accounts[selectedAddress]?.balance + ? (hexToBN(accounts[selectedAddress].balance) as any)?.isZero?.() + : undefined, + }); } else { Logger.error('FiatOnRampAgg::ApplePay empty order response', order); } @@ -73,13 +94,16 @@ const ApplePayButton = ({ setLockTime(prevLockTime); } }, [ + accounts, addOrder, lockTime, navigation, - network, + chainId, pay, protectWalletModalVisible, quote.crypto?.symbol, + selectedAddress, + trackEvent, ]); return ; diff --git a/app/components/UI/FiatOnRampAggregator/hooks/useAnalytics.ts b/app/components/UI/FiatOnRampAggregator/hooks/useAnalytics.ts new file mode 100644 index 00000000000..1865ba6eee5 --- /dev/null +++ b/app/components/UI/FiatOnRampAggregator/hooks/useAnalytics.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react'; +import { InteractionManager } from 'react-native'; +import Analytics from '../../../../core/Analytics'; +import { ANALYTICS_EVENTS_V2 } from '../../../../util/analyticsV2'; +import { AnalyticsEvents } from '../types'; + +const AnonymousEvents: (keyof AnalyticsEvents)[] = [ + 'ONRAMP_REGION_SELECTED', + 'ONRAMP_PAYMENT_METHOD_SELECTED', + 'ONRAMP_QUOTES_REQUESTED', + 'ONRAMP_QUOTES_RECEIVED', + 'ONRAMP_PROVIDER_SELECTED', + 'ONRAMP_PURCHASE_COMPLETED', + 'ONRAMP_PURCHASE_FAILED', + 'ONRAMP_PROVIDER_DETAILS_VIEWED', + 'ONRAMP_QUOTE_ERROR', +]; + +function useAnalytics() { + const trackEvent = useCallback( + ( + eventType: T, + params: AnalyticsEvents[T], + ) => { + const event = ANALYTICS_EVENTS_V2[eventType]; + const anonymous = AnonymousEvents.includes(eventType); + + InteractionManager.runAfterInteractions(() => { + if (anonymous) { + Analytics.trackEventWithParameters(event, {}); + Analytics.trackEventWithParameters(event, params, true); + } else { + Analytics.trackEventWithParameters(event, params); + } + }); + }, + [], + ); + + return trackEvent; +} + +export default useAnalytics; diff --git a/app/components/UI/FiatOnRampAggregator/orderProcessor/aggregator.ts b/app/components/UI/FiatOnRampAggregator/orderProcessor/aggregator.ts index 55d1e1ddcce..ff573f84c01 100644 --- a/app/components/UI/FiatOnRampAggregator/orderProcessor/aggregator.ts +++ b/app/components/UI/FiatOnRampAggregator/orderProcessor/aggregator.ts @@ -34,7 +34,7 @@ const aggregatorOrderStateToFiatOrderState = ( interface InitialAggregatorOrder { id: string; account: string; - network: number; + network: string; } export const aggregatorInitialFiatOrder = ( @@ -63,7 +63,9 @@ export const aggregatorOrderToFiatOrder = (aggregatorOrder: Order) => ({ currency: aggregatorOrder.fiatCurrency?.symbol || '', currencySymbol: aggregatorOrder.fiatCurrency?.denomSymbol || '', cryptocurrency: aggregatorOrder.cryptoCurrency?.symbol || '', - network: aggregatorOrder.cryptoCurrency?.network?.chainId || '', + network: + aggregatorOrder.cryptoCurrency?.network?.chainId && + String(aggregatorOrder.cryptoCurrency.network.chainId), state: aggregatorOrderStateToFiatOrderState(aggregatorOrder.status), account: aggregatorOrder.walletAddress, txHash: aggregatorOrder.txHash, @@ -98,5 +100,3 @@ export async function processAggregatorOrder( return order; } } - -export const callbackBaseUrl = 'https://dummy.url.metamask.io'; diff --git a/app/components/UI/FiatOnRampAggregator/sdk/index.tsx b/app/components/UI/FiatOnRampAggregator/sdk/index.tsx index 3cf5ffc913d..c7d3ee9b94d 100644 --- a/app/components/UI/FiatOnRampAggregator/sdk/index.tsx +++ b/app/components/UI/FiatOnRampAggregator/sdk/index.tsx @@ -57,6 +57,7 @@ export interface IFiatOnRampSDK { selectedChainId: string; appConfig: IFiatOnRampSDKConfig; + callbackBaseUrl: string; } interface IProviderProps { @@ -75,6 +76,10 @@ export const SDK = OnRampSdk.create( }, ); +export const callbackBaseUrl = isDevelopment + ? 'https://on-ramp-content.metaswap-dev.codefi.network/regions/fake-callback' + : 'https://on-ramp-content.metaswap.codefi.network/regions/fake-callback'; + const appConfig = { POLLING_INTERVAL: 20000, POLLING_INTERVAL_HIGHLIGHT: 10000, @@ -96,6 +101,10 @@ export const FiatOnRampSDKProvider = ({ const sdk = await SDK.regions(); setSdkModule(sdk); } catch (error) { + Logger.error( + error as Error, + `FiatOnRampSDKProvider SDK.regions() failed`, + ); setSdkError(error as Error); } })(); @@ -180,6 +189,7 @@ export const FiatOnRampSDKProvider = ({ selectedChainId, appConfig, + callbackBaseUrl, }; return ; diff --git a/app/components/UI/FiatOnRampAggregator/types/analytics.ts b/app/components/UI/FiatOnRampAggregator/types/analytics.ts new file mode 100644 index 00000000000..513eaf3047a --- /dev/null +++ b/app/components/UI/FiatOnRampAggregator/types/analytics.ts @@ -0,0 +1,130 @@ +export type ScreenLocation = + | 'Amount to Buy Screen' + | 'Payment Method Screen' + | 'Region Screen' + | 'Quotes Screen' + | 'Provider Webview' + | 'Get Started Screen' + | 'Order Details Screen'; + +export interface AnalyticsEvents { + BUY_BUTTON_CLICKED: { + text: 'Buy' | 'Buy Native Token'; + location: string; + chain_id_destination: string; + }; + ONRAMP_REGION_SELECTED: { + is_supported: boolean; + country_onramp_id: string; + state_onramp_id?: string; + location?: ScreenLocation; + }; + ONRAMP_PAYMENT_METHOD_SELECTED: { + payment_method_id: string; + location?: ScreenLocation; + }; + ONRAMP_QUOTES_REQUESTED: { + currency_source: string; + currency_destination: string; + payment_method_id: string; + chain_id_destination: string; + amount: number; + location: ScreenLocation; + }; + ONRAMP_CANCELED: { + location: ScreenLocation; + chain_id_destination: string; + provider_onramp?: string; + results_count?: number; + }; + ONRAMP_QUOTES_RECEIVED: { + currency_source: string; + currency_destination: string; + chain_id_destination: string; + amount: number; + refresh_count: number; + results_count: number; + average_crypto_out: number; + average_total_fee: number; + average_gas_fee: number; + average_processing_fee: number; + provider_onramp_list: string[]; + provider_onramp_first: string; + average_total_fee_of_amount: number; + provider_onramp_last?: string; + }; + ONRAMP_PROVIDER_SELECTED: { + provider_onramp: string; + refresh_count: number; + quote_position: number; + results_count: number; + crypto_out: number; + currency_source: string; + currency_destination: string; + chain_id_destination: string; + total_fee: number; + gas_fee: number; + processing_fee: number; + exchange_rate: number; + }; + ONRAMP_PROVIDER_DETAILS_VIEWED: { + provider_onramp: string; + }; + ONRAMP_PURCHASE_SUBMITTED: { + provider_onramp: string; + chain_id_destination: string; + has_zero_native_balance?: boolean; + is_apple_pay: boolean; + }; + ONRAMP_PURCHASE_COMPLETED: { + crypto_out: number; + currency_source: string; + currency_destination: string; + chain_id_destination: string; + total_fee: number; + exchange_rate: number; + payment_method_id: string; + provider_onramp: string; + gas_fee?: number; + processing_fee?: number; + }; + ONRAMP_PURCHASE_FAILED: { + currency_source: string; + currency_destination: string; + chain_id_destination: string; + payment_method_id: string; + provider_onramp: string; + }; + ONRAMP_PURCHASE_CANCELLED: { + currency_source: string; + currency_destination: string; + chain_id_destination: string; + payment_method_id: string; + provider_onramp: string; + }; + ONRAMP_PURCHASE_DETAILS_VIEWED: { + purchase_status: string; + provider_onramp: string; + currency_destination: string; + chain_id_destination: string; + currency_source: string; + }; + ONRAMP_EXTERNAL_LINK_CLICKED: { + location: ScreenLocation; + text: + | 'Etherscan Transaction' + | 'Provider Order Tracking' + | 'Provider Homepage' + | 'Provider Support' + | 'Provider Privacy Policy' + | 'Provider Terms of Service'; + url_domain: string; + }; + ONRAMP_QUOTE_ERROR: { + provider_onramp_list: string[]; + currency_source: string; + currency_destination: string; + chain_id_destination: string; + amount: number; + }; +} diff --git a/app/components/UI/FiatOnRampAggregator/types/index.ts b/app/components/UI/FiatOnRampAggregator/types/index.ts index 985b6786995..6ff963e8ce8 100644 --- a/app/components/UI/FiatOnRampAggregator/types/index.ts +++ b/app/components/UI/FiatOnRampAggregator/types/index.ts @@ -7,3 +7,5 @@ export enum PROVIDER_LINKS { PRIVACY_POLICY = 'Privacy Policy', SUPPORT = 'Support', } + +export * from './analytics'; diff --git a/app/components/UI/FiatOrders/MoonPayWebView/index.js b/app/components/UI/FiatOrders/MoonPayWebView/index.js index be77d6a3ff2..789846159db 100644 --- a/app/components/UI/FiatOrders/MoonPayWebView/index.js +++ b/app/components/UI/FiatOrders/MoonPayWebView/index.js @@ -108,7 +108,7 @@ class MoonPayWebView extends PureComponent { this.props.protectWalletModalVisible(); InteractionManager.runAfterInteractions(() => { AnalyticsV2.trackEvent( - AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_SUBMITTED, + AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_SUBMITTED_LEGACY, { fiat_amount: { value: order.amount, anonymous: true }, fiat_currency: { value: order.currency, anonymous: true }, diff --git a/app/components/UI/FiatOrders/PaymentMethodApplePay/index.js b/app/components/UI/FiatOrders/PaymentMethodApplePay/index.js index ff3b9dfba88..df8050633fd 100644 --- a/app/components/UI/FiatOrders/PaymentMethodApplePay/index.js +++ b/app/components/UI/FiatOrders/PaymentMethodApplePay/index.js @@ -348,7 +348,7 @@ function PaymentMethodApplePay({ getNotificationDetails(order), ); AnalyticsV2.trackEvent( - AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_SUBMITTED, + AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_SUBMITTED_LEGACY, { fiat_amount: { value: order.amount, anonymous: true }, fiat_currency: { value: order.currency, anonymous: true }, diff --git a/app/components/UI/FiatOrders/TransakWebView/index.js b/app/components/UI/FiatOrders/TransakWebView/index.js index f565fdd7544..7602286f85e 100644 --- a/app/components/UI/FiatOrders/TransakWebView/index.js +++ b/app/components/UI/FiatOrders/TransakWebView/index.js @@ -106,7 +106,7 @@ class TransakWebView extends PureComponent { this.props.navigation.dangerouslyGetParent()?.pop(); InteractionManager.runAfterInteractions(() => { AnalyticsV2.trackEvent( - AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_SUBMITTED, + AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_SUBMITTED_LEGACY, { fiat_amount: { value: order.amount, anonymous: true }, fiat_currency: { value: order.currency, anonymous: true }, diff --git a/app/components/UI/FiatOrders/index.js b/app/components/UI/FiatOrders/index.js index 6350a480b6e..92e5f3a49c6 100644 --- a/app/components/UI/FiatOrders/index.js +++ b/app/components/UI/FiatOrders/index.js @@ -7,6 +7,7 @@ import NotificationManager from '../../../core/NotificationManager'; import { strings } from '../../../../locales/i18n'; import { renderNumber } from '../../../util/number'; import { + FIAT_ORDER_PROVIDERS, FIAT_ORDER_STATES, NETWORKS_CHAIN_ID, } from '../../../constants/on-ramp'; @@ -16,6 +17,7 @@ import { } from '../../../reducers/fiatOrders'; import useInterval from '../../hooks/useInterval'; import processOrder from '../FiatOnRampAggregator/orderProcessor'; +import useAnalytics from '../FiatOnRampAggregator/hooks/useAnalytics'; /** * @typedef {import('../../../reducers/fiatOrders').FiatOrder} FiatOrder @@ -34,7 +36,6 @@ export const allowedToBuy = (chainId) => NETWORKS_CHAIN_ID.ARBITRUM, NETWORKS_CHAIN_ID.CELO, NETWORKS_CHAIN_ID.AVAXCCHAIN, - NETWORKS_CHAIN_ID.HARMONY, ].includes(chainId); const baseNotificationDetails = { @@ -58,13 +59,59 @@ export const getAnalyticsPayload = (fiatOrder) => { }; switch (fiatOrder.state) { case FIAT_ORDER_STATES.FAILED: { - return [AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_FAILED, payload]; + return [ + AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_FAILED_LEGACY, + payload, + ]; } case FIAT_ORDER_STATES.CANCELLED: { - return [AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_CANCELLED, payload]; + return [ + AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_CANCELLED_LEGACY, + payload, + ]; } case FIAT_ORDER_STATES.COMPLETED: { - return [AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_COMPLETED, payload]; + return [ + AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_PURCHASE_COMPLETED_LEGACY, + payload, + ]; + } + case FIAT_ORDER_STATES.PENDING: + default: { + return [null]; + } + } +}; +/** + * @param {FiatOrder} fiatOrder + */ +export const getAggregatorAnalyticsPayload = (fiatOrder) => { + const failedOrCancelledParams = { + currency_source: fiatOrder.currency, + currency_destination: fiatOrder.cryptocurrency, + chain_id_destination: fiatOrder.network, + payment_method_id: fiatOrder.data?.paymentMethod?.id, + provider_onramp: fiatOrder.data?.provider?.name, + }; + + const completedPayload = { + ...failedOrCancelledParams, + crypto_out: fiatOrder.amount, + total_fee: fiatOrder.total_fee, + exchange_rate: + (Number(fiatOrder.amount) - Number(fiatOrder.cryptoFee)) / + Number(fiatOrder.cryptoAmount), + }; + + switch (fiatOrder.state) { + case FIAT_ORDER_STATES.FAILED: { + return ['ONRAMP_PURCHASE_FAILED', failedOrCancelledParams]; + } + case FIAT_ORDER_STATES.CANCELLED: { + return ['ONRAMP_PURCHASE_CANCELLED', failedOrCancelledParams]; + } + case FIAT_ORDER_STATES.COMPLETED: { + return ['ONRAMP_PURCHASE_COMPLETED', completedPayload]; } case FIAT_ORDER_STATES.PENDING: default: { @@ -133,6 +180,7 @@ export const getNotificationDetails = (fiatOrder) => { }; function FiatOrders({ pendingOrders, updateFiatOrder }) { + const trackEvent = useAnalytics(); useInterval( async () => { await Promise.all( @@ -140,6 +188,14 @@ function FiatOrders({ pendingOrders, updateFiatOrder }) { const updatedOrder = await processOrder(order); updateFiatOrder(updatedOrder); if (updatedOrder.state !== order.state) { + if (updatedOrder.provider === FIAT_ORDER_PROVIDERS.AGGREGATOR) { + const [event, params] = + getAggregatorAnalyticsPayload(updatedOrder); + if (event) { + trackEvent(event, params); + } + return; + } InteractionManager.runAfterInteractions(() => { const [analyticsEvent, analyticsPayload] = getAnalyticsPayload(updatedOrder); diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index bcc97deea68..d15b7f9c7df 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -1422,6 +1422,7 @@ export function getFiatOnRampAggNavbar( navigation, { title, showBack = true } = {}, themeColors, + onCancel, ) { const innerStyles = StyleSheet.create({ headerButtonText: { @@ -1467,9 +1468,11 @@ export function getFiatOnRampAggNavbar( ); }, headerRight: () => ( - // eslint-disable-next-line react/jsx-no-bind navigation.dangerouslyGetParent()?.pop()} + onPress={() => { + navigation.dangerouslyGetParent()?.pop(); + onCancel?.(); + }} style={styles.closeButton} > diff --git a/app/components/UI/ReceiveRequest/index.js b/app/components/UI/ReceiveRequest/index.js index c2861a86408..93a0adaf28d 100644 --- a/app/components/UI/ReceiveRequest/index.js +++ b/app/components/UI/ReceiveRequest/index.js @@ -38,6 +38,7 @@ import GlobalAlert from '../GlobalAlert'; import StyledButton from '../StyledButton'; import ClipboardManager from '../../../core/ClipboardManager'; import { ThemeContext, mockTheme } from '../../../util/theme'; +import Routes from '../../../constants/navigation/Routes'; const createStyles = (colors) => StyleSheet.create({ @@ -182,13 +183,16 @@ class ReceiveRequest extends PureComponent { ); } else { toggleReceiveModal(); - navigation.navigate('FiatOnRampAggregator'); + navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.ID); InteractionManager.runAfterInteractions(() => { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_OPENED, { - button_location: 'Receive Modal', - button_copy: `Buy ${this.props.ticker}`, - }); + Analytics.trackEventWithParameters( + AnalyticsV2.ANALYTICS_EVENTS.BUY_BUTTON_CLICKED, + { + text: 'Buy Native Token', + location: 'Receive Modal', + chain_id_destination: this.props.chainId, + }, + ); }); } }; diff --git a/app/components/UI/SelectComponent/__snapshots__/index.test.tsx.snap b/app/components/UI/SelectComponent/__snapshots__/index.test.tsx.snap index 26d5caa3814..801796ee7f9 100644 --- a/app/components/UI/SelectComponent/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/SelectComponent/__snapshots__/index.test.tsx.snap @@ -161,8 +161,10 @@ exports[`SelectComponent should render correctly 1`] = ` onPress={[Function]} style={ Object { + "alignItems": "center", "flexDirection": "row", "height": 35, + "justifyContent": "center", "paddingHorizontal": 15, "paddingVertical": 5, } @@ -188,8 +190,10 @@ exports[`SelectComponent should render correctly 1`] = ` onPress={[Function]} style={ Object { + "alignItems": "center", "flexDirection": "row", "height": 35, + "justifyContent": "center", "paddingHorizontal": 15, "paddingVertical": 5, } diff --git a/app/components/UI/SelectComponent/index.js b/app/components/UI/SelectComponent/index.js index 8027b4646de..ec99a1bc705 100644 --- a/app/components/UI/SelectComponent/index.js +++ b/app/components/UI/SelectComponent/index.js @@ -71,6 +71,8 @@ const createStyles = (colors) => paddingHorizontal: 15, paddingVertical: 5, flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', height: Device.isIos() ? ROW_HEIGHT : undefined, }, optionLabel: { @@ -81,7 +83,6 @@ const createStyles = (colors) => }, icon: { paddingHorizontal: 10, - marginTop: 5, }, listWrapper: { flex: 1, diff --git a/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap b/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap index 46746ac8b0b..afe8b3f1d71 100644 --- a/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap @@ -2,6 +2,7 @@ exports[`Tokens should hide zero balance tokens when setting is on 1`] = ` { this.props.navigation.navigate('FiatOnRampAggregator'); InteractionManager.runAfterInteractions(() => { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_OPENED, { - button_location: 'Home Screen', - button_copy: 'Buy ETH', - }); + Analytics.trackEventWithParameters( + AnalyticsV2.ANALYTICS_EVENTS.BUY_BUTTON_CLICKED, + { + text: 'Buy Native Token', + location: 'Home Screen', + chain_id_destination: this.props.chainId, + }, + ); }); }; @@ -361,6 +368,7 @@ class Tokens extends PureComponent { } const mapStateToProps = (state) => ({ + chainId: state.engine.backgroundState.NetworkController.provider.chainId, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, conversionRate: diff --git a/app/components/Views/RevealPrivateCredential/index.js b/app/components/Views/RevealPrivateCredential/index.js index 96240687ce2..cb1b4b8ce00 100644 --- a/app/components/Views/RevealPrivateCredential/index.js +++ b/app/components/Views/RevealPrivateCredential/index.js @@ -590,25 +590,25 @@ class RevealPrivateCredential extends PureComponent { const { styles } = this.getStyles(); return ( - {strings('reveal_credential.seed_phrase_explanation')[0]} - Linking.openURL(SRP_URL)}> - - {strings('reveal_credential.seed_phrase_explanation')[1]} - - - {strings('reveal_credential.seed_phrase_explanation')[2]} + {strings('reveal_credential.seed_phrase_explanation')[0]}{' '} + Linking.openURL(SRP_URL)} + > + {strings('reveal_credential.seed_phrase_explanation')[1]} + {' '} + {strings('reveal_credential.seed_phrase_explanation')[2]}{' '} {strings('reveal_credential.seed_phrase_explanation')[3]} - {strings('reveal_credential.seed_phrase_explanation')[4]} - Linking.openURL(NON_CUSTODIAL_WALLET_URL)} > - - {strings('reveal_credential.seed_phrase_explanation')[5]} - - - {strings('reveal_credential.seed_phrase_explanation')[6]} + {strings('reveal_credential.seed_phrase_explanation')[5]}{' '} + + {strings('reveal_credential.seed_phrase_explanation')[6]}{' '} {strings('reveal_credential.seed_phrase_explanation')[7]} diff --git a/app/components/Views/SendFlow/SendTo/index.js b/app/components/Views/SendFlow/SendTo/index.js index 16b0ac58b5b..485a1543d39 100644 --- a/app/components/Views/SendFlow/SendTo/index.js +++ b/app/components/Views/SendFlow/SendTo/index.js @@ -51,6 +51,7 @@ import { ADD_ADDRESS_MODAL_CONTAINER_ID, ENTER_ALIAS_INPUT_BOX_ID, } from '../../../../constants/test-ids'; +import Routes from '../../../../constants/navigation/Routes'; const { hexToBN } = util; const createStyles = (colors) => @@ -193,6 +194,10 @@ class SendFlow extends PureComponent { * Map representing the address book */ addressBook: PropTypes.object, + /** + * Network provider chain id + */ + chainId: PropTypes.string, /** * Network id */ @@ -358,8 +363,27 @@ class SendFlow extends PureComponent { }); this.toggleFromAccountModal(); }; + /** + * This returns the address name from the address book or user accounts if the selectedAddress exist there + * @param {String} toSelectedAddress - Address input + * @returns {String | null} - Address or null if toSelectedAddress is not in the addressBook or identities array + */ + getAddressNameFromBookOrIdentities = (toSelectedAddress) => { + if (!toSelectedAddress) return; + + const { addressBook, network, identities } = this.props; + const networkAddressBook = addressBook[network] || {}; + + const checksummedAddress = toChecksumAddress(toSelectedAddress); - validateAddress = async (toSelectedAddress) => { + return networkAddressBook[checksummedAddress] + ? networkAddressBook[checksummedAddress].name + : identities[checksummedAddress] + ? identities[checksummedAddress].name + : null; + }; + + validateAddressOrENSFromInput = async (toSelectedAddress) => { const { AssetsContractController } = Engine.context; const { addressBook, network, identities, providerType } = this.props; const networkAddressBook = addressBook[network] || {}; @@ -369,8 +393,7 @@ class SendFlow extends PureComponent { errorContinue, isOnlyWarning, confusableCollection, - toEnsAddressResolved, - isFromAddressBook; + toEnsAddressResolved; let [addToAddressToAddressBook, toSelectedAddressReady] = [false, false]; if (isValidAddress(toSelectedAddress)) { const checksummedToSelectedAddress = toChecksumAddress(toSelectedAddress); @@ -385,18 +408,11 @@ class SendFlow extends PureComponent { addToAddressToAddressBook = true; } } else if ( - networkAddressBook[checksummedToSelectedAddress] || - identities[checksummedToSelectedAddress] + !networkAddressBook[checksummedToSelectedAddress] && + !identities[checksummedToSelectedAddress] ) { - toAddressName = - (networkAddressBook[checksummedToSelectedAddress] && - networkAddressBook[checksummedToSelectedAddress].name) || - (identities[checksummedToSelectedAddress] && - identities[checksummedToSelectedAddress].name); - isFromAddressBook = true; - } else { - toAddressName = ens || toSelectedAddress; - // If not in address book nor user accounts + toAddressName = toSelectedAddress; + // If not in the addressBook nor user accounts addToAddressToAddressBook = true; } @@ -461,25 +477,46 @@ class SendFlow extends PureComponent { } else if (toSelectedAddress && toSelectedAddress.length >= 42) { addressError = strings('transaction.invalid_address'); } + this.setState({ addressError, addToAddressToAddressBook, toSelectedAddressReady, - toSelectedAddressName: toAddressName, toEnsName, + toSelectedAddressName: toAddressName, errorContinue, isOnlyWarning, confusableCollection, toEnsAddressResolved, - isFromAddressBook, }); }; onToSelectedAddressChange = (toSelectedAddress) => { - this.validateAddress(toSelectedAddress); - this.setState({ - toSelectedAddress, - }); + const addressName = + this.getAddressNameFromBookOrIdentities(toSelectedAddress); + + /** + * If the address is from addressBook or identities + * then validation is not necessary since it was already validated + */ + if (addressName) { + this.setState({ + toSelectedAddress, + toSelectedAddressReady: true, + isFromAddressBook: true, + toSelectedAddressName: addressName, + }); + } else { + this.validateAddressOrENSFromInput(toSelectedAddress); + /** + * Because validateAddressOrENSFromInput is an asynchronous function + * we are setting the state here synchronously, so it does not block the UI + * */ + this.setState({ + toSelectedAddress, + isFromAddressBook: false, + }); + } }; validateToAddress = async () => { @@ -661,12 +698,12 @@ class SendFlow extends PureComponent { }; goToBuy = () => { - this.props.navigation.navigate('FiatOnRampAggregator'); + this.props.navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.ID); InteractionManager.runAfterInteractions(() => { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.ONRAMP_OPENED, { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BUY_BUTTON_CLICKED, { button_location: 'Send Flow warning', - button_copy: 'Buy ETH', + button_copy: 'Buy Native Token', + chain_id_destination: this.props.chainId, }); }); }; @@ -885,6 +922,7 @@ SendFlow.contextType = ThemeContext; const mapStateToProps = (state) => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, addressBook: state.engine.backgroundState.AddressBookController.addressBook, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, selectedAsset: state.transaction.selectedAsset, diff --git a/app/core/DeeplinkManager.js b/app/core/DeeplinkManager.js index 06c151938ac..9fb8efee47d 100644 --- a/app/core/DeeplinkManager.js +++ b/app/core/DeeplinkManager.js @@ -322,6 +322,7 @@ class DeeplinkManager { // For ex. go to settings case PROTOCOLS.METAMASK: handled(); + if (url.startsWith('metamask://wc')) { const cleanUrlObj = new URL(urlObj.query.replace('?uri=', '')); const href = cleanUrlObj.href; @@ -334,13 +335,8 @@ class DeeplinkManager { params?.autosign, origin, ); - } else if (url.startsWith('metamask://dapp/')) { - try { - this._handleBrowserUrl(urlObj.href.split('metamask://dapp/')[1]); - } catch (e) { - if (e) Alert.alert(strings('deeplink.invalid'), e.toString()); - } } + break; default: return false; diff --git a/app/util/analytics.js b/app/util/analytics.js index fe5428ce31d..999e05e612f 100644 --- a/app/util/analytics.js +++ b/app/util/analytics.js @@ -97,8 +97,6 @@ const NAMES = { DAPP_APPROVE_SCREEN_EDIT_PERMISSION: 'Edit permission', DAPP_APPROVE_SCREEN_EDIT_FEE: 'Edit tx fee', DAPP_APPROVE_SCREEN_VIEW_DETAILS: 'View tx details', - // Fiat Orders - WALLET_BUY_ETH: 'Buy ETH', PAYMENTS_SELECTS_DEBIT_OR_ACH: 'Selects debit card or bank account as payment method', PAYMENTS_SELECTS_APPLE_PAY: 'Selects Apple Pay as payment method', @@ -559,12 +557,6 @@ export const ANALYTICS_EVENT_OPTS = { ACTIONS.APPROVE_REQUEST, NAMES.DAPP_APPROVE_SCREEN_VIEW_DETAILS, ), - // Fiat Orders - WALLET_BUY_ETH: generateOpt( - CATEGORIES.WALLET, - ACTIONS.BUY_ETH, - NAMES.WALLET_BUY_ETH, - ), PAYMENTS_SELECTS_DEBIT_OR_ACH: generateOpt( CATEGORIES.PAYMENTS, ACTIONS.SELECTS_DEBIT_OR_ACH, diff --git a/app/util/analyticsV2.js b/app/util/analyticsV2.js index 6e4d197ec36..f4d0d4c3c9a 100644 --- a/app/util/analyticsV2.js +++ b/app/util/analyticsV2.js @@ -39,16 +39,16 @@ export const ANALYTICS_EVENTS_V2 = { // Send transaction SEND_TRANSACTION_STARTED: generateOpt('Send Transaction Started'), SEND_TRANSACTION_COMPLETED: generateOpt('Send Transaction Completed'), - // On-ramp + // On-ramp [LEGACY] ONRAMP_OPENED: generateOpt('On-ramp Opened'), ONRAMP_CLOSED: generateOpt('On-ramp Closed'), ONRAMP_PURCHASE_EXITED: generateOpt('On-ramp Purchase Exited'), ONRAMP_PURCHASE_STARTED: generateOpt('On-ramp Purchase Started'), ONRAMP_PURCHASE_SUBMISSION_FAILED: generateOpt('On-ramp Submission Failed'), - ONRAMP_PURCHASE_SUBMITTED: generateOpt('On-ramp Purchase Submitted'), - ONRAMP_PURCHASE_FAILED: generateOpt('On-ramp Purchase Failed'), - ONRAMP_PURCHASE_CANCELLED: generateOpt('On-ramp Purchase Cancelled'), - ONRAMP_PURCHASE_COMPLETED: generateOpt('On-ramp Purchase Completed'), + ONRAMP_PURCHASE_SUBMITTED_LEGACY: generateOpt('On-ramp Purchase Submitted'), + ONRAMP_PURCHASE_FAILED_LEGACY: generateOpt('On-ramp Purchase Failed'), + ONRAMP_PURCHASE_CANCELLED_LEGACY: generateOpt('On-ramp Purchase Cancelled'), + ONRAMP_PURCHASE_COMPLETED_LEGACY: generateOpt('On-ramp Purchase Completed'), // Wallet Security WALLET_SECURITY_STARTED: generateOpt('Wallet Security Started'), WALLET_SECURITY_MANUAL_BACKUP_INITIATED: generateOpt( @@ -131,6 +131,29 @@ export const ANALYTICS_EVENTS_V2 = { 'User canceled QR hardware transaction', ), HARDWARE_WALLET_ERROR: generateOpt('Hardware wallet error'), + + // ONRAMP AGGREGATOR + BUY_BUTTON_CLICKED: generateOpt('Buy Button Clicked'), + ONRAMP_REGION_SELECTED: generateOpt('On-ramp Region Selected'), + ONRAMP_PAYMENT_METHOD_SELECTED: generateOpt( + 'On-ramp Payment Method Selected', + ), + ONRAMP_QUOTES_REQUESTED: generateOpt('On-ramp Quotes Requested'), + ONRAMP_CANCELED: generateOpt('On-ramp Canceled'), + ONRAMP_QUOTES_RECEIVED: generateOpt('On-ramp Quotes Received'), + ONRAMP_PROVIDER_SELECTED: generateOpt('On-ramp Provider Selected'), + ONRAMP_PROVIDER_DETAILS_VIEWED: generateOpt( + 'On-ramp Provider Details Viewed', + ), + ONRAMP_PURCHASE_SUBMITTED: generateOpt('On-ramp Purchase Submitted'), + ONRAMP_PURCHASE_COMPLETED: generateOpt('On-ramp Purchase Completed'), + ONRAMP_PURCHASE_FAILED: generateOpt('On-ramp Purchase Failed'), + ONRAMP_PURCHASE_CANCELLED: generateOpt('On-ramp Purchase Cancelled'), + ONRAMP_PURCHASE_DETAILS_VIEWED: generateOpt( + 'On-ramp Purchase Details Viewed', + ), + ONRAMP_EXTERNAL_LINK_CLICKED: generateOpt('External Link Clicked'), + ONRAMP_QUOTE_ERROR: generateOpt('On-ramp Quote Error'), }; /** diff --git a/bitrise.yml b/bitrise.yml index c7f34dc97f9..2b6246c6e29 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -323,10 +323,10 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 5.1.1 + VERSION_NAME: 5.2.0 - opts: is_expand: false - VERSION_NUMBER: 906 + VERSION_NUMBER: 913 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index a3f8c0b74ca..7b3204cc634 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -825,7 +825,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 906; + CURRENT_PROJECT_VERSION = 913; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -861,7 +861,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.1.1; + MARKETING_VERSION = 5.2.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -892,7 +892,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 906; + CURRENT_PROJECT_VERSION = 913; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( @@ -927,7 +927,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.1.1; + MARKETING_VERSION = 5.2.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/locales/languages/el.json b/locales/languages/el.json index b62e4332675..9cdbe436e2c 100644 --- a/locales/languages/el.json +++ b/locales/languages/el.json @@ -1771,6 +1771,10 @@ "loading": "Φόρτωση...", "not_found": "Τα μέσα δεν βρέθηκαν" }, + "secret_phrase_video_subtitle": { + "title": "EL CC", + "language": "el" + }, "edit_gas_fee_eip1559": { "advanced_options": "Επιλογές για προχωρημένους", "gas_limit": "Όριο τέλους συναλλαγής", diff --git a/locales/languages/en.json b/locales/languages/en.json index 9b65e963271..a4d6ee081cf 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -611,13 +611,13 @@ "done": "Done", "confirm": "Next", "seed_phrase_explanation": [ - "The ", + "The", "Secret Recovery Phrase (SRP)", - " gives ", + "gives", "full access to your wallet, funds and accounts.\n\n", - "MetaMask is a ", - "non-custodial wallet", - ". That means, ", + "MetaMask is a", + "non-custodial wallet.", + "That means,", "you are the owner of your SRP." ], "private_key_explanation": "Save it somewhere safe and secret.", @@ -1598,7 +1598,9 @@ "no_providers_available": "No providers available", "try_different_amount_to_buy_input": "Try choosing a different payment method or try to increase or reduce the amount you want to buy!", "webview_received_error": "WebView received error status code: {{code}}", - "no_tokens_available": "There are currently no cryptocurrencies available to purchase on {{network}}", + "no_tokens_available": "There are currently no cryptocurrencies available to purchase on {{network}} in {{region}}", + "try_different_region": "Try a different region", + "return_home": "Return to Home Screen", "region": { "buy_crypto_tokens": "Buy Crypto Tokens", "title": "Select your Region", @@ -1614,19 +1616,19 @@ "unsupported_description": "We are working hard to expand coverage to your region as soon as we can. In the meantime, see our support article for other ways you may be able to buy crypto.", "unsupported_link": "Visit Support Article" }, - "transaction": { - "details_main": "Transaction Details", - "successful": "Transaction Successful!", + "order_details": { + "details_main": "Order Details", + "successful": "Order Successful!", "your": "Your", "available_in_account": "is now available in your account", "crypto": "crypto", - "failed": "Transaction Failed", - "failed_description": "Something went wrong, and {{provider}} was unable to complete your transaction. Please try again or with another provider.", + "failed": "Order Failed", + "failed_description": "Something went wrong, and {{provider}} was unable to complete your order. Please try again or with another provider.", "the_provider": "the provider", - "processing": "Processing Transaction", + "processing": "Processing Order", "processing_card_description": "Credit/Debit purchases typically take a few minutes", "processing_bank_description": "Bank transfers typically take a few business days", - "details": "Transaction details", + "details": "Order details", "via": "via", "purchase_amount": "Purchase Amount Total", "etherscan": "View full details on", @@ -1634,7 +1636,7 @@ "questions": "Questions?", "contact": "Contact", "support": "Support", - "id": "Transaction ID", + "id": "Order ID", "date_and_time": "Date and Time", "payment_method": "Payment Method", "token_amount": "Token Amount", diff --git a/locales/languages/fr.json b/locales/languages/fr.json index 4fb51c44ef8..956f8987177 100644 --- a/locales/languages/fr.json +++ b/locales/languages/fr.json @@ -1771,6 +1771,10 @@ "loading": "Chargement…", "not_found": "Média introuvable" }, + "secret_phrase_video_subtitle": { + "title": "FR CC", + "language": "fr" + }, "edit_gas_fee_eip1559": { "advanced_options": "Options avancées", "gas_limit": "Limite de gaz", diff --git a/locales/languages/hi.json b/locales/languages/hi.json index 42c8cd9e2a9..1cb7adf3553 100644 --- a/locales/languages/hi.json +++ b/locales/languages/hi.json @@ -1771,6 +1771,11 @@ "loading": "लोड हो रहा है...", "not_found": "माध्यम नहीं मिला" }, + "secret_phrase_video_subtitle": { + "title": "HI CC", + "language": "hi", + "uri": "https://github.com/MetaMask/metamask-mobile/blob/main/app/videos/subtitles/secretPhrase/subtitles-hi-in.vtt?raw=true" + }, "edit_gas_fee_eip1559": { "advanced_options": "एडवांस विकल्प", "gas_limit": "गैस की सीमा", diff --git a/locales/languages/id.json b/locales/languages/id.json index 095ba9520ee..95b78029243 100644 --- a/locales/languages/id.json +++ b/locales/languages/id.json @@ -1771,6 +1771,11 @@ "loading": "Memuat...", "not_found": "Media tidak ditemukan" }, + "secret_phrase_video_subtitle": { + "title": "ID CC", + "language": "id", + "uri": "https://github.com/MetaMask/metamask-mobile/blob/main/app/videos/subtitles/secretPhrase/subtitles-id-id.vtt?raw=true" + }, "edit_gas_fee_eip1559": { "advanced_options": "Opsi lanjutan", "gas_limit": "Batas gas", diff --git a/locales/languages/ja.json b/locales/languages/ja.json index 28ca3a80c30..39b79f0e09b 100644 --- a/locales/languages/ja.json +++ b/locales/languages/ja.json @@ -1771,6 +1771,11 @@ "loading": "読み込み中...", "not_found": "メディアが見つかりません" }, + "secret_phrase_video_subtitle": { + "title": "JA CC", + "language": "ja", + "uri": "https://github.com/MetaMask/metamask-mobile/blob/main/app/videos/subtitles/secretPhrase/subtitles-ja-jp.vtt?raw=true" + }, "edit_gas_fee_eip1559": { "advanced_options": "高度なオプション", "gas_limit": "ガス上限", diff --git a/locales/languages/ko.json b/locales/languages/ko.json index 3c0f54b46f4..0a6455afe01 100644 --- a/locales/languages/ko.json +++ b/locales/languages/ko.json @@ -1771,6 +1771,11 @@ "loading": "로딩 중...", "not_found": "미디어가 확인되지 않음" }, + "secret_phrase_video_subtitle": { + "title": "KO CC", + "language": "ko", + "uri": "https://github.com/MetaMask/metamask-mobile/blob/main/app/videos/subtitles/secretPhrase/subtitles-ko-kr.vtt?raw=true" + }, "edit_gas_fee_eip1559": { "advanced_options": "고급 옵션", "gas_limit": "가스 한도", diff --git a/locales/languages/pt.json b/locales/languages/pt.json index 460b1fe901a..f535762429f 100644 --- a/locales/languages/pt.json +++ b/locales/languages/pt.json @@ -1771,6 +1771,11 @@ "loading": "Carregando...", "not_found": "Mídia não encontrada" }, + "secret_phrase_video_subtitle": { + "title": "PT CC", + "language": "pt", + "uri": "https://github.com/MetaMask/metamask-mobile/blob/main/app/videos/subtitles/secretPhrase/subtitles-pt-br.vtt?raw=true" + }, "edit_gas_fee_eip1559": { "advanced_options": "Opções avançadas", "gas_limit": "Limite de Gas", diff --git a/locales/languages/ru.json b/locales/languages/ru.json index 83d304b727a..a96e9e2480a 100644 --- a/locales/languages/ru.json +++ b/locales/languages/ru.json @@ -1771,6 +1771,11 @@ "loading": "Загрузка...", "not_found": "Медиа не найдено" }, + "secret_phrase_video_subtitle": { + "title": "RU CC", + "language": "ru", + "uri": "https://github.com/MetaMask/metamask-mobile/blob/main/app/videos/subtitles/secretPhrase/subtitles-ru-ru.vtt?raw=true" + }, "edit_gas_fee_eip1559": { "advanced_options": "Расширенные параметры", "gas_limit": "Лимит газа", diff --git a/locales/languages/tr.json b/locales/languages/tr.json index 4a1d44fb7c7..d8088de37c1 100644 --- a/locales/languages/tr.json +++ b/locales/languages/tr.json @@ -1771,6 +1771,10 @@ "loading": "Yükleniyor...", "not_found": "Ortam bulunamadı" }, + "secret_phrase_video_subtitle": { + "title": "TR CC", + "language": "tr" + }, "edit_gas_fee_eip1559": { "advanced_options": "Gelişmiş seçenekler", "gas_limit": "Gaz limiti", diff --git a/locales/languages/vi.json b/locales/languages/vi.json index d40335b97ca..e7f9b0d3685 100644 --- a/locales/languages/vi.json +++ b/locales/languages/vi.json @@ -1771,6 +1771,11 @@ "loading": "Đang tải...", "not_found": "Không tìm thấy phương tiện" }, + "secret_phrase_video_subtitle": { + "title": "VI CC", + "language": "vi", + "uri": "https://github.com/MetaMask/metamask-mobile/blob/main/app/videos/subtitles/secretPhrase/subtitles-vi-vn.vtt?raw=true" + }, "edit_gas_fee_eip1559": { "advanced_options": "Tùy chọn nâng cao", "gas_limit": "Giới hạn gas", diff --git a/package.json b/package.json index ea3441a6b81..5d5a0e022c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "5.1.1", + "version": "5.2.0", "private": true, "scripts": { "audit:ci": "./scripts/yarn-audit.sh", @@ -104,7 +104,7 @@ "oss-attribution-generator/**/debug": "^2.6.9" }, "dependencies": { - "@consensys/on-ramp-sdk": "^1.0.2", + "@consensys/on-ramp-sdk": "^1.0.3", "@exodus/react-native-payments": "git+https://github.com/MetaMask/react-native-payments.git#dbc8cbbed570892d2fea5e3d183bf243e062c1e5", "@keystonehq/bc-ur-registry-eth": "^0.7.7", "@keystonehq/metamask-airgapped-keyring": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index 0e8db525eeb..e760d50fc75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1104,10 +1104,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@consensys/on-ramp-sdk@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@consensys/on-ramp-sdk/-/on-ramp-sdk-1.0.2.tgz#b450227649cfa9084fd9c4b2d0e098c8f4bc2ca3" - integrity sha512-9hIuRrCsrifwVV+kPkuTw8wIJODAczZgNbWHvPdaWMGy3Qv1rQ0O9bmLHiO56Wrym/U+2bBpCdiwPtMiJiKUHA== +"@consensys/on-ramp-sdk@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@consensys/on-ramp-sdk/-/on-ramp-sdk-1.0.3.tgz#92cc982d00cde4b3749ec5024ce67a4a74d5cbf2" + integrity sha512-1WRuInJ5WTt1NHFVejI7BgTCRhYAQAlOg0wd3l60dgD1NqS3KI3z6hUqve3EPZGq/R/0DbQ8vjxBPl/rRNNxDg== dependencies: async "^3.2.3" axios "^0.21.0"