Skip to content

Commit

Permalink
feat: second user model PR with wallet properties (#4206)
Browse files Browse the repository at this point in the history
* init commit

* remove absolute value in date calc

* all the events are now logged properly plus changed native token address to NATIVE

* add documentation line

* remove unnecessary prop

* init

* init

* checkpoint

* checkpoint

* merge

* lint

* cleanup

* wallet user model stuff working as expected now

* add app loaded event and rest of user properties

* fix tests

* change token balances as per kyle rec

* refactor connected wallet state handling + rest of vm comments

* fix redux breaking, revert wallet from set to array
  • Loading branch information
lynnshaoyu committed Aug 3, 2022
1 parent ec783fd commit ed5902a
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 34 deletions.
39 changes: 32 additions & 7 deletions src/components/AmplitudeAnalytics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* and logged.
*/
export enum EventName {
APP_LOADED = 'Application Loaded',
APPROVE_TOKEN_TXN_SUBMITTED = 'Approve Token Transaction Submitted',
CONNECT_WALLET_BUTTON_CLICKED = 'Connect Wallet Button Clicked',
PAGE_VIEWED = 'Page Viewed',
Expand All @@ -26,15 +27,39 @@ export enum EventName {
}

export enum CUSTOM_USER_PROPERTIES {
WALLET_ADDRESS = 'wallet_address',
WALLET_TYPE = 'wallet_type',
USER_LAST_SEEN_DATE = 'user_last_seen_date',
USER_FIRST_SEEN_DATE = 'user_first_seen_date',
WALLET_CHAIN_IDS = 'all_wallet_chain_ids',
ALL_WALLET_ADDRESSES_CONNECTED = 'all_wallet_addresses_connected',
SCREEN_RESOLUTION = 'screen_resolution',
ALL_WALLET_CHAIN_IDS = 'all_wallet_chain_ids',
BROWSER = 'browser',
LIGHT_MODE = 'light_mode',
DARK_MODE = 'is_dark_mode',
SCREEN_RESOLUTION_HEIGHT = 'screen_resolution_height',
SCREEN_RESOLUTION_WIDTH = 'screen_resolution_width',
WALLET_ADDRESS = 'wallet_address',
WALLET_NATIVE_CURRENCY_BALANCE_USD = 'wallet_native_currency_balance_usd',
WALLET_TOKENS_ADDRESSES = 'wallet_tokens_addresses',
WALLET_TOKENS_SYMBOLS = 'wallet_tokens_symbols',
WALLET_TYPE = 'wallet_type',
}

export enum CUSTOM_USER_PROPERTY_SUFFIXES {
WALLET_TOKEN_AMOUNT_SUFFIX = '_token_amount',
}

export enum CUSTOM_USER_PROPERTY_PREFIXES {
WALLET_CHAIN_IDS_PREFIX = 'wallet_chain_ids_',
WALLET_FIRST_SEEN_DATE_PREFIX = 'first_seen_date_',
WALLET_LAST_SEEN_DATE_PREFIX = 'last_seen_date_',
}

export enum BROWSER {
FIREFOX = 'Mozilla Firefox',
SAMSUNG = 'Samsung Internet',
OPERA = 'Opera',
INTERNET_EXPLORER = 'Microsoft Internet Explorer',
EDGE = 'Microsoft Edge (Legacy)',
EDGE_CHROMIUM = 'Microsoft Edge (Chromium)',
CHROME = 'Google Chrome or Chromium',
SAFARI = 'Apple Safari',
UNKNOWN = 'unknown',
}

export enum WALLET_CONNECTION_RESULT {
Expand Down
10 changes: 6 additions & 4 deletions src/components/AmplitudeAnalytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function sendAnalyticsEvent(eventName: string, eventProperties?: Record<s
track(eventName, eventProperties)
}

type Value = string | number | boolean | string[] | number[]

/**
* Class that exposes methods to mutate the User Model's properties in
* Amplitude that represents the current session's user.
Expand All @@ -67,16 +69,16 @@ class UserModel {
identify(mutate(new Identify()))
}

set(key: string, value: string | number) {
set(key: string, value: Value) {
this.call((event) => event.set(key, value))
}

setOnce(key: string, value: string | number) {
setOnce(key: string, value: Value) {
this.call((event) => event.setOnce(key, value))
}

add(key: string, value: string | number) {
this.call((event) => event.add(key, typeof value === 'number' ? value : 0))
add(key: string, value: number) {
this.call((event) => event.add(key, value))
}

postInsert(key: string, value: string | number) {
Expand Down
10 changes: 5 additions & 5 deletions src/components/SearchModal/CurrencySearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ export function CurrencySearch({
}, [allTokens, debouncedQuery])

const [balances, balancesIsLoading] = useAllTokenBalances()
const sortedTokens: Token[] = useMemo(() => {
void balancesIsLoading // creates a new array once balances load to update hooks
return [...filteredTokens].sort(tokenComparator.bind(null, balances))
}, [balances, filteredTokens, balancesIsLoading])
const sortedTokens: Token[] = useMemo(
() => (!balancesIsLoading ? [...filteredTokens].sort(tokenComparator.bind(null, balances)) : []),
[balances, filteredTokens, balancesIsLoading]
)

const filteredSortedTokens = useSortTokensByQuery(debouncedQuery, sortedTokens)

Expand All @@ -126,7 +126,7 @@ export function CurrencySearch({
const s = debouncedQuery.toLowerCase().trim()
if (native.symbol?.toLowerCase()?.indexOf(s) !== -1) {
// Always bump the native token to the top of the list.
return native ? [native, ...filteredSortedTokens.filter((t) => !t.equals(native))] : filteredSortedTokens
return [native, ...filteredSortedTokens.filter((t) => !t.equals(native))]
}
return filteredSortedTokens
}, [debouncedQuery, native, filteredSortedTokens])
Expand Down
40 changes: 40 additions & 0 deletions src/components/WalletModal/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import * as connectionUtils from 'connection/utils'
import JSBI from 'jsbi'
import { ApplicationModal } from 'state/application/reducer'

import { nativeOnChain } from '../../constants/tokens'
import { render, screen } from '../../test-utils'
import WalletModal from './index'

Expand All @@ -9,6 +12,11 @@ afterEach(() => {
jest.resetModules()
})

const currencyAmount = (token: Currency, amount: number) => CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount))

const mockEth = () => nativeOnChain(1)
const mockCurrencyAmount = currencyAmount(mockEth(), 1)

const UserAgentMock = jest.requireMock('utils/userAgent')
jest.mock('utils/userAgent', () => ({
isMobile: false,
Expand All @@ -23,6 +31,38 @@ jest.mock('.../../state/application/hooks', () => {
}
})

jest.mock('hooks/useStablecoinPrice', () => {
return {
useStablecoinValue: (_currencyAmount: CurrencyAmount<Currency> | undefined | null) => {
return
},
}
})

jest.mock('state/connection/hooks', () => {
return {
useAllTokenBalances: () => {
return [{}, false]
},
}
})

jest.mock('../../hooks/Tokens', () => {
return {
useAllTokens: () => ({}),
}
})

jest.mock('lib/hooks/useCurrencyBalance', () => {
return {
useCurrencyBalances: (account?: string, currencies?: (Currency | undefined)[]) => {
return [mockCurrencyAmount]
},
}
})

jest.mock('lib/hooks/useNativeCurrency', () => () => mockEth)

jest.mock('@web3-react/core', () => {
const web3React = jest.requireActual('@web3-react/core')
return {
Expand Down
105 changes: 89 additions & 16 deletions src/components/WalletModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { sendAnalyticsEvent, user } from 'components/AmplitudeAnalytics'
import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'components/AmplitudeAnalytics/constants'
import {
CUSTOM_USER_PROPERTIES,
CUSTOM_USER_PROPERTY_PREFIXES,
CUSTOM_USER_PROPERTY_SUFFIXES,
EventName,
WALLET_CONNECTION_RESULT,
} from 'components/AmplitudeAnalytics/constants'
import { formatToDecimal, getTokenAddress } from 'components/AmplitudeAnalytics/utils'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { ConnectionType } from 'connection'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsInjected, getIsMetaMask } from 'connection/utils'
import { useCallback, useEffect, useState } from 'react'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { tokenComparator } from 'lib/hooks/useTokenList/sorting'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { useAllTokenBalances } from 'state/connection/hooks'
import { updateConnectionError } from 'state/connection/reducer'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
Expand All @@ -18,6 +31,7 @@ import styled from 'styled-components/macro'
import { isMobile } from 'utils/userAgent'

import { ReactComponent as Close } from '../../assets/images/x.svg'
import { useAllTokens } from '../../hooks/Tokens'
import { useModalIsOpen, useToggleWalletModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { ExternalLink, ThemedText } from '../../theme'
Expand Down Expand Up @@ -114,25 +128,51 @@ const WALLET_VIEWS = {
PENDING: 'pending',
}

const sendAnalyticsWalletBalanceUserInfo = (
balances: (CurrencyAmount<Currency> | undefined)[],
nativeCurrencyBalanceUsd: number
) => {
const walletTokensSymbols: string[] = []
const walletTokensAddresses: string[] = []
balances.forEach((currencyAmount) => {
if (currencyAmount !== undefined) {
const tokenBalanceAmount = formatToDecimal(currencyAmount, currencyAmount.currency.decimals)
if (tokenBalanceAmount > 0) {
const tokenAddress = getTokenAddress(currencyAmount.currency)
walletTokensAddresses.push(getTokenAddress(currencyAmount.currency))
walletTokensSymbols.push(currencyAmount.currency.symbol ?? '')
const tokenPrefix = currencyAmount.currency.symbol ?? tokenAddress
user.set(`${tokenPrefix}${CUSTOM_USER_PROPERTY_SUFFIXES.WALLET_TOKEN_AMOUNT_SUFFIX}`, tokenBalanceAmount)
}
}
})
user.set(CUSTOM_USER_PROPERTIES.WALLET_NATIVE_CURRENCY_BALANCE_USD, nativeCurrencyBalanceUsd)
user.set(CUSTOM_USER_PROPERTIES.WALLET_TOKENS_ADDRESSES, walletTokensAddresses)
user.set(CUSTOM_USER_PROPERTIES.WALLET_TOKENS_SYMBOLS, walletTokensSymbols)
}

const sendAnalyticsEventAndUserInfo = (
account: string,
walletType: string,
chainId: number | undefined,
isReconnect: boolean
) => {
const currentDate = new Date().toISOString()
sendAnalyticsEvent(EventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WALLET_CONNECTION_RESULT.SUCCEEDED,
wallet_address: account,
wallet_type: walletType,
is_reconnect: isReconnect,
})
const currentDate = new Date().toISOString()
user.set(CUSTOM_USER_PROPERTIES.WALLET_ADDRESS, account)
user.set(CUSTOM_USER_PROPERTIES.WALLET_TYPE, walletType)
if (chainId) user.postInsert(CUSTOM_USER_PROPERTIES.WALLET_CHAIN_IDS, chainId)
if (chainId) {
user.postInsert(CUSTOM_USER_PROPERTIES.ALL_WALLET_CHAIN_IDS, chainId)
user.postInsert(`${CUSTOM_USER_PROPERTY_PREFIXES.WALLET_CHAIN_IDS_PREFIX}${account}`, chainId)
}
user.postInsert(CUSTOM_USER_PROPERTIES.ALL_WALLET_ADDRESSES_CONNECTED, account)
user.setOnce(CUSTOM_USER_PROPERTIES.USER_FIRST_SEEN_DATE, currentDate)
user.set(CUSTOM_USER_PROPERTIES.USER_LAST_SEEN_DATE, currentDate)
user.setOnce(`${CUSTOM_USER_PROPERTY_PREFIXES.WALLET_FIRST_SEEN_DATE_PREFIX}${account}`, currentDate)
user.set(`${CUSTOM_USER_PROPERTY_PREFIXES.WALLET_LAST_SEEN_DATE_PREFIX}${account}`, currentDate)
}

export default function WalletModal({
Expand All @@ -146,10 +186,11 @@ export default function WalletModal({
}) {
const dispatch = useAppDispatch()
const { connector, account, chainId } = useWeb3React()
const [connectedWallets, updateConnectedWallets] = useConnectedWallets()
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()

const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
const [lastActiveWalletAddress, setLastActiveWalletAddress] = useState<string | undefined>(account)
const [shouldLogWalletBalances, setShouldLogWalletBalances] = useState(false)

const [pendingConnector, setPendingConnector] = useState<Connector | undefined>()
const pendingError = useAppSelector((state) =>
Expand All @@ -159,6 +200,25 @@ export default function WalletModal({
const walletModalOpen = useModalIsOpen(ApplicationModal.WALLET)
const toggleWalletModal = useToggleWalletModal()

const allTokens = useAllTokens()
const [tokenBalances, tokenBalancesIsLoading] = useAllTokenBalances()
const sortedTokens: Token[] = useMemo(
() => (!tokenBalancesIsLoading ? Object.values(allTokens).sort(tokenComparator.bind(null, tokenBalances)) : []),
[tokenBalances, allTokens, tokenBalancesIsLoading]
)
const native = useNativeCurrency()

const sortedTokensWithETH: Currency[] = useMemo(
() =>
// Always bump the native token to the top of the list.
native ? [native, ...sortedTokens.filter((t) => !t.equals(native))] : sortedTokens,
[native, sortedTokens]
)

const balances = useCurrencyBalances(account, sortedTokensWithETH)
const nativeBalance = balances.length > 0 ? balances[0] : null
const nativeCurrencyBalanceUsdValue = useStablecoinValue(nativeBalance)?.toFixed(2)

const openOptions = useCallback(() => {
setWalletView(WALLET_VIEWS.OPTIONS)
}, [setWalletView])
Expand All @@ -180,18 +240,31 @@ export default function WalletModal({
useEffect(() => {
if (account && account !== lastActiveWalletAddress) {
const walletType = getConnectionName(getConnection(connector).type, getIsMetaMask())

if (
const isReconnect =
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletType).length > 0
) {
sendAnalyticsEventAndUserInfo(account, walletType, chainId, true)
} else {
sendAnalyticsEventAndUserInfo(account, walletType, chainId, false)
updateConnectedWallets({ account, walletType })
}
sendAnalyticsEventAndUserInfo(account, walletType, chainId, isReconnect)
setShouldLogWalletBalances(true)
if (!isReconnect) addWalletToConnectedWallets({ account, walletType })
}
setLastActiveWalletAddress(account)
}, [connectedWallets, updateConnectedWallets, lastActiveWalletAddress, account, connector, chainId])
}, [connectedWallets, addWalletToConnectedWallets, lastActiveWalletAddress, account, connector, chainId])

// Send wallet balance info once it becomes available.
useEffect(() => {
if (!tokenBalancesIsLoading && shouldLogWalletBalances && balances && nativeCurrencyBalanceUsdValue) {
const nativeCurrencyBalanceUsd =
native && nativeCurrencyBalanceUsdValue ? parseFloat(nativeCurrencyBalanceUsdValue) : 0
sendAnalyticsWalletBalanceUserInfo(balances, nativeCurrencyBalanceUsd)
setShouldLogWalletBalances(false)
}
}, [
balances,
nativeCurrencyBalanceUsdValue,
shouldLogWalletBalances,
setShouldLogWalletBalances,
tokenBalancesIsLoading,
native,
])

const tryActivation = useCallback(
async (connector: Connector) => {
Expand Down
18 changes: 17 additions & 1 deletion src/pages/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { initializeAnalytics } from 'components/AmplitudeAnalytics'
import { PageName } from 'components/AmplitudeAnalytics/constants'
import { sendAnalyticsEvent, user } from 'components/AmplitudeAnalytics'
import { CUSTOM_USER_PROPERTIES, EventName, PageName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import Loader from 'components/Loader'
import TopLevelModals from 'components/TopLevelModals'
Expand All @@ -8,7 +9,9 @@ import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
import { lazy, Suspense } from 'react'
import { useEffect } from 'react'
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { getBrowser } from 'utils/browser'

import { useAnalyticsReporter } from '../components/analytics'
import ErrorBoundary from '../components/ErrorBoundary'
Expand Down Expand Up @@ -86,6 +89,7 @@ export default function App() {

const { pathname } = useLocation()
const currentPage = getCurrentPageFromLocation(pathname)
const isDarkMode = useIsDarkMode()

useAnalyticsReporter()
initializeAnalytics()
Expand All @@ -94,6 +98,18 @@ export default function App() {
window.scrollTo(0, 0)
}, [pathname])

useEffect(() => {
// TODO(zzmp): add web vitals event properties to app loaded event.
sendAnalyticsEvent(EventName.APP_LOADED)
user.set(CUSTOM_USER_PROPERTIES.BROWSER, getBrowser())
user.set(CUSTOM_USER_PROPERTIES.SCREEN_RESOLUTION_HEIGHT, window.screen.height)
user.set(CUSTOM_USER_PROPERTIES.SCREEN_RESOLUTION_WIDTH, window.screen.width)
}, [])

useEffect(() => {
user.set(CUSTOM_USER_PROPERTIES.DARK_MODE, isDarkMode)
}, [isDarkMode])

return (
<ErrorBoundary>
<DarkModeQueryParamReader />
Expand Down

1 comment on commit ed5902a

@vercel
Copy link

@vercel vercel bot commented on ed5902a Aug 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

interface – ./

interface-uniswap.vercel.app
interface-git-main-uniswap.vercel.app

Please sign in to comment.