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
This commit is contained in:
Kristie Huang 2023-11-08 16:18:16 -05:00 committed by GitHub
parent cee3390b71
commit 395b390df6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 190 additions and 220 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -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.

@ -0,0 +1,72 @@
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>
)
}

@ -0,0 +1,92 @@
import { ButtonText } from 'components/Button'
import { OpacityHoverState } from 'components/Common'
import { X } from 'react-feather'
import styled from 'styled-components'
import { BREAKPOINTS } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
export const PopupContainer = styled.div<{ show: boolean }>`
${({ show }) => !show && 'display: none'};
background-color: ${({ theme }) => theme.surface2};
color: ${({ theme }) => theme.neutral1};
position: fixed;
z-index: ${Z_INDEX.sticky};
user-select: none;
border-radius: 20px;
bottom: 40px;
right: 20px;
width: 360px;
height: 92px;
border: 1.3px solid ${({ theme }) => theme.surface3};
@media only screen and (max-width: ${BREAKPOINTS.md}px) {
bottom: 62px;
}
@media only screen and (max-width: ${BREAKPOINTS.xs}px) {
width: unset;
right: 10px;
left: 10px;
}
`
export const StyledXButton = styled(X)`
cursor: pointer;
position: absolute;
top: -30px;
right: 0px;
padding: 4px;
border-radius: 50%;
background-color: ${({ theme }) => theme.surface5};
color: ${({ theme }) => theme.neutral2};
${OpacityHoverState};
@media only screen and (max-width: ${BREAKPOINTS.xs}px) {
top: 8px;
right: 8px;
}
`
export const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
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};
`

@ -1,105 +0,0 @@
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>
)
}

@ -1,84 +0,0 @@
import walletBannerPhoneImageSrc from 'assets/images/wallet_banner_phone_image.png'
import { BaseButton } from 'components/Button'
import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { X } from 'react-feather'
import styled from 'styled-components'
import { Z_INDEX } from 'theme/zIndex'
export const PopupContainer = styled.div<{ show: boolean }>`
display: flex;
flex-direction: column;
justify-content: space-between;
${({ show }) => !show && 'display: none'};
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};
position: fixed;
z-index: ${Z_INDEX.sticky};
padding: 24px 16px 16px;
border-radius: 20px;
bottom: 20px;
right: 20px;
width: 390px;
height: 164px;
border: 1px solid ${({ theme }) => theme.surface3};
box-shadow: ${({ theme }) => theme.deprecated_deepShadow};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
bottom: 62px;
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
background-position: top 32px right -10px;
width: unset;
right: 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)`
cursor: pointer;
position: absolute;
top: 21px;
right: 17px;
color: ${({ theme }) => theme.white};
${OpacityHoverState};
`
export const BannerButton = styled(BaseButton)`
height: 40px;
border-radius: 16px;
padding: 10px;
${OpacityHoverState};
`

@ -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() {
<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 = {
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', () => {
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 = {
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 = {
}, },
pairs: {}, pairs: {},
timestamp: Date.now(), timestamp: Date.now(),
hideBaseWalletBanner: false, hideAndroidAnnouncementBanner: false,
}, },
_persist: { _persist: {
version: 2, version: 2,

@ -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'
import { import {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
updateHideBaseWalletBanner, updateHideAndroidAnnouncementBanner,
updateHideClosedPositions, updateHideClosedPositions,
updateUserDeadline, updateUserDeadline,
updateUserLocale, updateUserLocale,
@ -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, {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
initialState, initialState,
updateHideBaseWalletBanner, updateHideAndroidAnnouncementBanner,
updateHideClosedPositions, updateHideClosedPositions,
updateSelectedWallet, updateSelectedWallet,
updateUserDeadline, updateUserDeadline,
@ -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 {
} }
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 = {
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({
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 {
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
} 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) => {
}) })
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')
}