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>
<AutoColumn gap="4px">
<ThemedText.SubHeaderSmall color="neutral1">
<Trans>Don&apos;t have Uniswap Wallet?</Trans>
<Trans>Don&apos;t have a Uniswap wallet?</Trans>
</ThemedText.SubHeaderSmall>
<ThemedText.BodySmall color="neutral2">
{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>
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 UniwalletModal from 'components/AccountDrawer/UniwalletModal'
import AirdropModal from 'components/AirdropModal'
import BaseAnnouncementBanner from 'components/Banner/BaseAnnouncementBanner'
import AndroidAnnouncementBanner from 'components/Banner/AndroidAnnouncementBanner'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import FiatOnrampModal from 'components/FiatOnrampModal'
@ -30,7 +30,7 @@ export default function TopLevelModals() {
<ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
<Bag />
<UniwalletModal />
<BaseAnnouncementBanner />
<AndroidAnnouncementBanner />
<OffchainActivityModal />
<TransactionCompleteModal />
<AirdropModal />

@ -15,7 +15,7 @@ const previousState: PersistAppStateV1 = {
tokens: {},
pairs: {},
timestamp: Date.now(),
hideBaseWalletBanner: false,
hideAndroidAnnouncementBanner: false,
},
_persist: {
version: 0,
@ -43,7 +43,6 @@ describe('migration to v1', () => {
expect(result?.user?.tokens).toEqual({})
expect(result?.user?.pairs).toEqual({})
expect(result?.user?.timestamp).toEqual(previousState.user?.timestamp)
expect(result?.user?.hideBaseWalletBanner).toEqual(false)
})
it('should not migrate a non-default value', async () => {

@ -17,7 +17,7 @@ const previousState: PersistAppStateV2 = {
tokens: {},
pairs: {},
timestamp: Date.now(),
hideBaseWalletBanner: false,
hideAndroidAnnouncementBanner: false,
},
_persist: {
version: 1,

@ -38,7 +38,7 @@ const previousState: PersistAppStateV3 = {
},
pairs: {},
timestamp: Date.now(),
hideBaseWalletBanner: false,
hideAndroidAnnouncementBanner: false,
},
_persist: {
version: 2,

@ -87,7 +87,7 @@ interface ExpectedUserState {
}
}
timestamp: number
hideBaseWalletBanner: boolean
hideAndroidAnnouncementBanner: boolean
showSurveyPopup?: boolean
disabledUniswapX?: boolean
optedOutOfUniswapX?: boolean

@ -15,7 +15,7 @@ import { useDefaultActiveTokens } from '../../hooks/Tokens'
import {
addSerializedPair,
addSerializedToken,
updateHideBaseWalletBanner,
updateHideAndroidAnnouncementBanner,
updateHideClosedPositions,
updateUserDeadline,
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 hideBaseWalletBanner = useAppSelector((state) => state.user.hideBaseWalletBanner)
const hideAndroidAnnouncementBanner = useAppSelector((state) => state.user.hideAndroidAnnouncementBanner)
const toggleHideBaseWalletBanner = useCallback(() => {
dispatch(updateHideBaseWalletBanner({ hideBaseWalletBanner: true }))
const toggleHideAndroidAnnouncementBanner = useCallback(() => {
dispatch(updateHideAndroidAnnouncementBanner({ hideAndroidAnnouncementBanner: true }))
}, [dispatch])
return [hideBaseWalletBanner, toggleHideBaseWalletBanner]
return [hideAndroidAnnouncementBanner, toggleHideAndroidAnnouncementBanner]
}
export function useUserDisabledUniswapX(): boolean {

@ -5,7 +5,7 @@ import reducer, {
addSerializedPair,
addSerializedToken,
initialState,
updateHideBaseWalletBanner,
updateHideAndroidAnnouncementBanner,
updateHideClosedPositions,
updateSelectedWallet,
updateUserDeadline,
@ -77,10 +77,10 @@ describe('swap reducer', () => {
})
})
describe('updateHideBaseWalletBanner', () => {
it('updates the updateHideBaseWalletBanner', () => {
store.dispatch(updateHideBaseWalletBanner({ hideBaseWalletBanner: true }))
expect(store.getState().hideBaseWalletBanner).toEqual(true)
describe('updateHideAndroidAnnouncementBanner', () => {
it('updates the updateHideAndroidAnnouncementBanner', () => {
store.dispatch(updateHideAndroidAnnouncementBanner({ hideAndroidAnnouncementBanner: true }))
expect(store.getState().hideAndroidAnnouncementBanner).toEqual(true)
})
})

@ -47,7 +47,7 @@ export interface UserState {
}
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.
disabledUniswapX?: boolean
// temporary field indicating the user disabled UniswapX during the transition to the opt-out model
@ -73,7 +73,7 @@ export const initialState: UserState = {
tokens: {},
pairs: {},
timestamp: currentTimestamp(),
hideBaseWalletBanner: false,
hideAndroidAnnouncementBanner: false,
showSurveyPopup: undefined,
originCountry: undefined,
}
@ -108,8 +108,8 @@ const userSlice = createSlice({
updateHideClosedPositions(state, action) {
state.userHideClosedPositions = action.payload.userHideClosedPositions
},
updateHideBaseWalletBanner(state, action) {
state.hideBaseWalletBanner = action.payload.hideBaseWalletBanner
updateHideAndroidAnnouncementBanner(state, action) {
state.hideAndroidAnnouncementBanner = action.payload.hideAndroidAnnouncementBanner
},
updateDisabledUniswapX(state, action) {
state.disabledUniswapX = action.payload.disabledUniswapX
@ -152,7 +152,7 @@ export const {
updateUserDeadline,
updateUserLocale,
updateUserSlippageTolerance,
updateHideBaseWalletBanner,
updateHideAndroidAnnouncementBanner,
updateDisabledUniswapX,
updateOptedOutOfUniswapX,
} = userSlice.actions

@ -34,7 +34,8 @@ export function openDownloadApp({ element, isAndroidGALaunched }: OpenDownloadAp
} else if (isAndroidGALaunched && isAndroid) {
openDownloadStore({ element, appPlatform: AppDownloadPlatform.ANDROID, linkTarget: 'uniswap_wallet_playstore' })
} 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)
}
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')
}