Commit 395b390d authored by Kristie Huang's avatar Kristie Huang Committed by GitHub

feat: add android announcement banner (#7551)

* feat [wip]: add android announcement banner

* feat: [wip] add android announcement banner

* finish css

* remove hideBaseWallet references

* phil changes

* minor lint nit

* pr review

* growth copy
parent cee3390b
...@@ -111,11 +111,11 @@ function InfoSection() { ...@@ -111,11 +111,11 @@ function InfoSection() {
<InfoSectionWrapper> <InfoSectionWrapper>
<AutoColumn gap="4px"> <AutoColumn gap="4px">
<ThemedText.SubHeaderSmall color="neutral1"> <ThemedText.SubHeaderSmall color="neutral1">
<Trans>Don&apos;t have Uniswap Wallet?</Trans> <Trans>Don&apos;t have a Uniswap wallet?</Trans>
</ThemedText.SubHeaderSmall> </ThemedText.SubHeaderSmall>
<ThemedText.BodySmall color="neutral2"> <ThemedText.BodySmall color="neutral2">
{isAndroidGALaunched ? ( {isAndroidGALaunched ? (
<Trans>Get the Uniswap app on iOS and Android to safely store and swap tokens.</Trans> <Trans>Safely store and swap tokens with the Uniswap app. Available on iOS and Android.</Trans>
) : ( ) : (
<Trans> <Trans>
Download in the App Store to safely store your tokens and NFTs, swap tokens, and connect to crypto apps. Download in the App Store to safely store your tokens and NFTs, swap tokens, and connect to crypto apps.
......
import { Trans } from '@lingui/macro'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { useScreenSize } from 'hooks/useScreenSize'
import { useLocation } from 'react-router-dom'
import { useHideAndroidAnnouncementBanner } from 'state/user/hooks'
import { ThemedText } from 'theme/components'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { openDownloadApp } from 'utils/openDownloadApp'
import { isMobileSafari } from 'utils/userAgent'
import androidAnnouncementBannerQR from '../../../assets/images/androidAnnouncementBannerQR.png'
import darkAndroidThumbnail from '../../../assets/images/AndroidWallet-Thumbnail-Dark.png'
import lightAndroidThumbnail from '../../../assets/images/AndroidWallet-Thumbnail-Light.png'
import {
Container,
DownloadButton,
PopupContainer,
StyledQrCode,
StyledXButton,
TextContainer,
Thumbnail,
} from './styled'
export default function AndroidAnnouncementBanner() {
const [hideAndroidAnnouncementBanner, toggleHideAndroidAnnouncementBanner] = useHideAndroidAnnouncementBanner()
const location = useLocation()
const isLandingScreen = location.search === '?intro=true' || location.pathname === '/'
const screenSize = useScreenSize()
const shouldDisplay = Boolean(!hideAndroidAnnouncementBanner && !isLandingScreen)
const isDarkMode = useIsDarkMode()
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
const onClick = () =>
openDownloadApp({
element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON,
isAndroidGALaunched,
})
if (!isAndroidGALaunched || isMobileSafari) return null
return (
<PopupContainer show={shouldDisplay}>
<Container>
<Thumbnail src={isDarkMode ? darkAndroidThumbnail : lightAndroidThumbnail} alt="Android app thumbnail" />
<TextContainer onClick={!screenSize['xs'] ? onClick : undefined}>
<ThemedText.BodySmall lineHeight="20px">
<Trans>Uniswap on Android</Trans>
</ThemedText.BodySmall>
<ThemedText.LabelMicro>
<Trans>Available now - download from the Google Play Store today</Trans>
</ThemedText.LabelMicro>
<DownloadButton onClick={onClick}>
<Trans>Download now</Trans>
</DownloadButton>
</TextContainer>
<StyledQrCode src={androidAnnouncementBannerQR} alt="App OneLink QR code" />
<StyledXButton
data-testid="uniswap-wallet-banner"
size={24}
onClick={(e) => {
// prevent click from bubbling to UI on the page underneath, i.e. clicking a token row
e.preventDefault()
e.stopPropagation()
toggleHideAndroidAnnouncementBanner()
}}
/>
</Container>
</PopupContainer>
)
}
import walletBannerPhoneImageSrc from 'assets/images/wallet_banner_phone_image.png' import { ButtonText } from 'components/Button'
import { BaseButton } from 'components/Button'
import { OpacityHoverState } from 'components/Common' import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { X } from 'react-feather' import { X } from 'react-feather'
import styled from 'styled-components' import styled from 'styled-components'
import { BREAKPOINTS } from 'theme'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
export const PopupContainer = styled.div<{ show: boolean }>` export const PopupContainer = styled.div<{ show: boolean }>`
display: flex;
flex-direction: column;
justify-content: space-between;
${({ show }) => !show && 'display: none'}; ${({ show }) => !show && 'display: none'};
background-color: ${({ theme }) => theme.surface2};
background: url(${walletBannerPhoneImageSrc});
background-repeat: no-repeat;
background-position: top 18px right 15px;
background-size: 166px;
:hover {
background-size: 170px;
}
transition: background-size ${({ theme }) => theme.transition.duration.medium}
${({ theme }) => theme.transition.timing.inOut};
background-color: ${({ theme }) => theme.chain_84531};
color: ${({ theme }) => theme.neutral1}; color: ${({ theme }) => theme.neutral1};
position: fixed; position: fixed;
z-index: ${Z_INDEX.sticky}; z-index: ${Z_INDEX.sticky};
user-select: none;
padding: 24px 16px 16px;
border-radius: 20px; border-radius: 20px;
bottom: 20px; bottom: 40px;
right: 20px; right: 20px;
width: 390px; width: 360px;
height: 164px; height: 92px;
border: 1.3px solid ${({ theme }) => theme.surface3};
border: 1px solid ${({ theme }) => theme.surface3};
box-shadow: ${({ theme }) => theme.deprecated_deepShadow};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) { @media only screen and (max-width: ${BREAKPOINTS.md}px) {
bottom: 62px; bottom: 62px;
} }
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { @media only screen and (max-width: ${BREAKPOINTS.xs}px) {
background-position: top 32px right -10px;
width: unset; width: unset;
right: 10px; right: 10px;
left: 10px; left: 10px;
height: 144px;
} }
user-select: none;
`
export const BaseBackgroundImage = styled.img`
position: absolute;
top: 0;
left: 0;
height: 138px;
width: 138px;
`
export const ButtonRow = styled(Row)`
gap: 16px;
` `
export const StyledXButton = styled(X)` export const StyledXButton = styled(X)`
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
top: 21px; top: -30px;
right: 17px; right: 0px;
padding: 4px;
border-radius: 50%;
color: ${({ theme }) => theme.white}; background-color: ${({ theme }) => theme.surface5};
color: ${({ theme }) => theme.neutral2};
${OpacityHoverState}; ${OpacityHoverState};
@media only screen and (max-width: ${BREAKPOINTS.xs}px) {
top: 8px;
right: 8px;
}
` `
export const BannerButton = styled(BaseButton)` export const Container = styled.div`
height: 40px; display: flex;
border-radius: 16px; flex-direction: row;
padding: 10px; justify-content: flex-start;
${OpacityHoverState}; align-items: center;
height: 100%;
overflow: hidden;
border-radius: 20px;
gap: 16px;
`
export const Thumbnail = styled.img`
width: 82px;
`
export const TextContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
color: ${({ theme }) => theme.neutral2};
padding: 10px 0px 10px;
line-height: 16px;
@media only screen and (max-width: ${BREAKPOINTS.xs}px) {
width: 220px;
}
`
export const StyledQrCode = styled.img`
padding: 2px;
border-radius: 8px;
width: 64px;
height: 64px;
background-color: ${({ theme }) => theme.white};
margin-right: 16px;
@media only screen and (max-width: ${BREAKPOINTS.xs}px) {
display: none;
}
`
export const DownloadButton = styled(ButtonText)`
line-height: 16px;
font-size: 14px;
color: ${({ theme }) => theme.accent1};
` `
import { Trans } from '@lingui/macro'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { ChainId } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ReactComponent as AppleLogo } from 'assets/svg/apple_logo.svg'
import baseLogoUrl from 'assets/svg/base_background_icon.svg'
import { ReactComponent as UniswapAppLogo } from 'assets/svg/uniswap_app_logo.svg'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { useScreenSize } from 'hooks/useScreenSize'
import { useLocation } from 'react-router-dom'
import { useHideBaseWalletBanner } from 'state/user/hooks'
import { ThemedText } from 'theme/components'
import { openDownloadApp, openWalletMicrosite } from 'utils/openDownloadApp'
import { isAndroid, isIOS, isMobileSafari } from 'utils/userAgent'
import { BannerButton, BaseBackgroundImage, ButtonRow, PopupContainer, StyledXButton } from './styled'
export default function BaseWalletBanner() {
const { chainId } = useWeb3React()
const [hideBaseWalletBanner, toggleHideBaseWalletBanner] = useHideBaseWalletBanner()
const location = useLocation()
const isLandingScreen = location.search === '?intro=true' || location.pathname === '/'
const shouldDisplay = Boolean(!hideBaseWalletBanner && !isLandingScreen && chainId === ChainId.BASE)
const screenSize = useScreenSize()
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
if (isMobileSafari) return null
return (
<PopupContainer show={shouldDisplay}>
<StyledXButton
data-testid="uniswap-wallet-banner"
size={20}
onClick={(e) => {
// prevent click from bubbling to UI on the page underneath, i.e. clicking a token row
e.preventDefault()
e.stopPropagation()
toggleHideBaseWalletBanner()
}}
/>
<BaseBackgroundImage src={baseLogoUrl} alt="transparent base background logo" />
<ThemedText.HeadlineMedium fontSize="24px" lineHeight="28px" color="white" maxWidth="224px">
<Trans>
Swap on{' '}
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.5689 10C19.5689 15.4038 15.1806 19.7845 9.76737 19.7845C4.63163 19.7845 0.418433 15.8414 0 10.8225H12.9554V9.17755H0C0.418433 4.15863 4.63163 0.215576 9.76737 0.215576C15.1806 0.215576 19.5689 4.59621 19.5689 10Z"
fill="white"
/>
</svg>{' '}
BASE in the Uniswap wallet
</Trans>
</ThemedText.HeadlineMedium>
<ButtonRow>
{isIOS || (isAndroidGALaunched && isAndroid) ? (
<>
<BannerButton
backgroundColor="white"
onClick={() =>
openDownloadApp({
element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON,
isAndroidGALaunched,
})
}
>
{isAndroidGALaunched ? <UniswapAppLogo width={14} height={14} /> : <AppleLogo width={14} height={14} />}
<ThemedText.LabelSmall color="black" marginLeft="5px">
{!screenSize['xs'] ? <Trans>Download</Trans> : <Trans>Download app</Trans>}
</ThemedText.LabelSmall>
</BannerButton>
<BannerButton
backgroundColor="black"
onClick={() =>
openWalletMicrosite({ element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON })
}
>
<ThemedText.LabelSmall color="white">
<Trans>Learn more</Trans>
</ThemedText.LabelSmall>
</BannerButton>
</>
) : (
<BannerButton
backgroundColor="white"
width="125px"
onClick={() => openWalletMicrosite({ element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON })}
>
<ThemedText.LabelSmall color="black">
<Trans>Learn more</Trans>
</ThemedText.LabelSmall>
</BannerButton>
)}
</ButtonRow>
</PopupContainer>
)
}
...@@ -2,7 +2,7 @@ import { useWeb3React } from '@web3-react/core' ...@@ -2,7 +2,7 @@ import { useWeb3React } from '@web3-react/core'
import { OffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal' import { OffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal'
import UniwalletModal from 'components/AccountDrawer/UniwalletModal' import UniwalletModal from 'components/AccountDrawer/UniwalletModal'
import AirdropModal from 'components/AirdropModal' import AirdropModal from 'components/AirdropModal'
import BaseAnnouncementBanner from 'components/Banner/BaseAnnouncementBanner' import AndroidAnnouncementBanner from 'components/Banner/AndroidAnnouncementBanner'
import AddressClaimModal from 'components/claim/AddressClaimModal' import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked' import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import FiatOnrampModal from 'components/FiatOnrampModal' import FiatOnrampModal from 'components/FiatOnrampModal'
...@@ -30,7 +30,7 @@ export default function TopLevelModals() { ...@@ -30,7 +30,7 @@ export default function TopLevelModals() {
<ConnectedAccountBlocked account={account} isOpen={accountBlocked} /> <ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
<Bag /> <Bag />
<UniwalletModal /> <UniwalletModal />
<BaseAnnouncementBanner /> <AndroidAnnouncementBanner />
<OffchainActivityModal /> <OffchainActivityModal />
<TransactionCompleteModal /> <TransactionCompleteModal />
<AirdropModal /> <AirdropModal />
......
...@@ -15,7 +15,7 @@ const previousState: PersistAppStateV1 = { ...@@ -15,7 +15,7 @@ const previousState: PersistAppStateV1 = {
tokens: {}, tokens: {},
pairs: {}, pairs: {},
timestamp: Date.now(), timestamp: Date.now(),
hideBaseWalletBanner: false, hideAndroidAnnouncementBanner: false,
}, },
_persist: { _persist: {
version: 0, version: 0,
...@@ -43,7 +43,6 @@ describe('migration to v1', () => { ...@@ -43,7 +43,6 @@ describe('migration to v1', () => {
expect(result?.user?.tokens).toEqual({}) expect(result?.user?.tokens).toEqual({})
expect(result?.user?.pairs).toEqual({}) expect(result?.user?.pairs).toEqual({})
expect(result?.user?.timestamp).toEqual(previousState.user?.timestamp) expect(result?.user?.timestamp).toEqual(previousState.user?.timestamp)
expect(result?.user?.hideBaseWalletBanner).toEqual(false)
}) })
it('should not migrate a non-default value', async () => { it('should not migrate a non-default value', async () => {
......
...@@ -17,7 +17,7 @@ const previousState: PersistAppStateV2 = { ...@@ -17,7 +17,7 @@ const previousState: PersistAppStateV2 = {
tokens: {}, tokens: {},
pairs: {}, pairs: {},
timestamp: Date.now(), timestamp: Date.now(),
hideBaseWalletBanner: false, hideAndroidAnnouncementBanner: false,
}, },
_persist: { _persist: {
version: 1, version: 1,
......
...@@ -38,7 +38,7 @@ const previousState: PersistAppStateV3 = { ...@@ -38,7 +38,7 @@ const previousState: PersistAppStateV3 = {
}, },
pairs: {}, pairs: {},
timestamp: Date.now(), timestamp: Date.now(),
hideBaseWalletBanner: false, hideAndroidAnnouncementBanner: false,
}, },
_persist: { _persist: {
version: 2, version: 2,
......
...@@ -87,7 +87,7 @@ interface ExpectedUserState { ...@@ -87,7 +87,7 @@ interface ExpectedUserState {
} }
} }
timestamp: number timestamp: number
hideBaseWalletBanner: boolean hideAndroidAnnouncementBanner: boolean
showSurveyPopup?: boolean showSurveyPopup?: boolean
disabledUniswapX?: boolean disabledUniswapX?: boolean
optedOutOfUniswapX?: boolean optedOutOfUniswapX?: boolean
......
...@@ -15,7 +15,7 @@ import { useDefaultActiveTokens } from '../../hooks/Tokens' ...@@ -15,7 +15,7 @@ import { useDefaultActiveTokens } from '../../hooks/Tokens'
import { import {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
updateHideBaseWalletBanner, updateHideAndroidAnnouncementBanner,
updateHideClosedPositions, updateHideClosedPositions,
updateUserDeadline, updateUserDeadline,
updateUserLocale, updateUserLocale,
...@@ -206,15 +206,15 @@ export function usePairAdder(): (pair: Pair) => void { ...@@ -206,15 +206,15 @@ export function usePairAdder(): (pair: Pair) => void {
) )
} }
export function useHideBaseWalletBanner(): [boolean, () => void] { export function useHideAndroidAnnouncementBanner(): [boolean, () => void] {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const hideBaseWalletBanner = useAppSelector((state) => state.user.hideBaseWalletBanner) const hideAndroidAnnouncementBanner = useAppSelector((state) => state.user.hideAndroidAnnouncementBanner)
const toggleHideBaseWalletBanner = useCallback(() => { const toggleHideAndroidAnnouncementBanner = useCallback(() => {
dispatch(updateHideBaseWalletBanner({ hideBaseWalletBanner: true })) dispatch(updateHideAndroidAnnouncementBanner({ hideAndroidAnnouncementBanner: true }))
}, [dispatch]) }, [dispatch])
return [hideBaseWalletBanner, toggleHideBaseWalletBanner] return [hideAndroidAnnouncementBanner, toggleHideAndroidAnnouncementBanner]
} }
export function useUserDisabledUniswapX(): boolean { export function useUserDisabledUniswapX(): boolean {
......
...@@ -5,7 +5,7 @@ import reducer, { ...@@ -5,7 +5,7 @@ import reducer, {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
initialState, initialState,
updateHideBaseWalletBanner, updateHideAndroidAnnouncementBanner,
updateHideClosedPositions, updateHideClosedPositions,
updateSelectedWallet, updateSelectedWallet,
updateUserDeadline, updateUserDeadline,
...@@ -77,10 +77,10 @@ describe('swap reducer', () => { ...@@ -77,10 +77,10 @@ describe('swap reducer', () => {
}) })
}) })
describe('updateHideBaseWalletBanner', () => { describe('updateHideAndroidAnnouncementBanner', () => {
it('updates the updateHideBaseWalletBanner', () => { it('updates the updateHideAndroidAnnouncementBanner', () => {
store.dispatch(updateHideBaseWalletBanner({ hideBaseWalletBanner: true })) store.dispatch(updateHideAndroidAnnouncementBanner({ hideAndroidAnnouncementBanner: true }))
expect(store.getState().hideBaseWalletBanner).toEqual(true) expect(store.getState().hideAndroidAnnouncementBanner).toEqual(true)
}) })
}) })
......
...@@ -47,7 +47,7 @@ export interface UserState { ...@@ -47,7 +47,7 @@ export interface UserState {
} }
timestamp: number timestamp: number
hideBaseWalletBanner: boolean hideAndroidAnnouncementBanner: boolean
// legacy field indicating the user disabled UniswapX during the opt-in period, or dismissed the UniswapX opt-in modal. // legacy field indicating the user disabled UniswapX during the opt-in period, or dismissed the UniswapX opt-in modal.
disabledUniswapX?: boolean disabledUniswapX?: boolean
// temporary field indicating the user disabled UniswapX during the transition to the opt-out model // temporary field indicating the user disabled UniswapX during the transition to the opt-out model
...@@ -73,7 +73,7 @@ export const initialState: UserState = { ...@@ -73,7 +73,7 @@ export const initialState: UserState = {
tokens: {}, tokens: {},
pairs: {}, pairs: {},
timestamp: currentTimestamp(), timestamp: currentTimestamp(),
hideBaseWalletBanner: false, hideAndroidAnnouncementBanner: false,
showSurveyPopup: undefined, showSurveyPopup: undefined,
originCountry: undefined, originCountry: undefined,
} }
...@@ -108,8 +108,8 @@ const userSlice = createSlice({ ...@@ -108,8 +108,8 @@ const userSlice = createSlice({
updateHideClosedPositions(state, action) { updateHideClosedPositions(state, action) {
state.userHideClosedPositions = action.payload.userHideClosedPositions state.userHideClosedPositions = action.payload.userHideClosedPositions
}, },
updateHideBaseWalletBanner(state, action) { updateHideAndroidAnnouncementBanner(state, action) {
state.hideBaseWalletBanner = action.payload.hideBaseWalletBanner state.hideAndroidAnnouncementBanner = action.payload.hideAndroidAnnouncementBanner
}, },
updateDisabledUniswapX(state, action) { updateDisabledUniswapX(state, action) {
state.disabledUniswapX = action.payload.disabledUniswapX state.disabledUniswapX = action.payload.disabledUniswapX
...@@ -152,7 +152,7 @@ export const { ...@@ -152,7 +152,7 @@ export const {
updateUserDeadline, updateUserDeadline,
updateUserLocale, updateUserLocale,
updateUserSlippageTolerance, updateUserSlippageTolerance,
updateHideBaseWalletBanner, updateHideAndroidAnnouncementBanner,
updateDisabledUniswapX, updateDisabledUniswapX,
updateOptedOutOfUniswapX, updateOptedOutOfUniswapX,
} = userSlice.actions } = userSlice.actions
......
...@@ -34,7 +34,8 @@ export function openDownloadApp({ element, isAndroidGALaunched }: OpenDownloadAp ...@@ -34,7 +34,8 @@ export function openDownloadApp({ element, isAndroidGALaunched }: OpenDownloadAp
} else if (isAndroidGALaunched && isAndroid) { } else if (isAndroidGALaunched && isAndroid) {
openDownloadStore({ element, appPlatform: AppDownloadPlatform.ANDROID, linkTarget: 'uniswap_wallet_playstore' }) openDownloadStore({ element, appPlatform: AppDownloadPlatform.ANDROID, linkTarget: 'uniswap_wallet_playstore' })
} else { } else {
openWalletMicrosite({ element }) sendAnalyticsEvent(InterfaceEventName.UNISWAP_WALLET_MICROSITE_OPENED, { element })
window.open(APP_DOWNLOAD_LINKS[element], /* target = */ 'uniswap_wallet_microsite')
} }
} }
...@@ -61,8 +62,3 @@ const openDownloadStore = (options: AnalyticsLinkOptions) => { ...@@ -61,8 +62,3 @@ const openDownloadStore = (options: AnalyticsLinkOptions) => {
}) })
window.open(APP_DOWNLOAD_LINKS[options.element], /* target = */ options.linkTarget) window.open(APP_DOWNLOAD_LINKS[options.element], /* target = */ options.linkTarget)
} }
export const openWalletMicrosite = (options: AnalyticsLinkOptions) => {
sendAnalyticsEvent(InterfaceEventName.UNISWAP_WALLET_MICROSITE_OPENED, { element: options.element })
window.open(APP_DOWNLOAD_LINKS[options.element], /* target = */ 'uniswap_wallet_microsite')
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment