feat(moonpay): add iframe (#9)

* feat(moonpay): add iframe

* add prod url

* polish iframe styles and supply the firebase fn with the new platform query param

* pr feedback

* pr feedback - .production env key distinction

feat(moonpay): add iframe (#9)

* feat(moonpay): add iframe

* add prod url

* polish iframe styles and supply the firebase fn with the new platform query param

* pr feedback

* pr feedback - .production env key distinction
This commit is contained in:
Jordan Frankfurt 2022-12-05 10:40:24 -06:00
parent 46d9d8e3df
commit f753a5e325
9 changed files with 292 additions and 25 deletions

6
.env

@ -1,7 +1,11 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
# These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
ESLINT_NO_DEV_ERRORS=true
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"

@ -1,7 +1,10 @@
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink"
REACT_APP_MOONPAY_PUBLISHABLE_KEY=""
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"

@ -0,0 +1,121 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { useCallback, useEffect, useState } from 'react'
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled, { useTheme } from 'styled-components/macro'
import { CustomLightSpinner, ThemedText } from 'theme'
import Circle from '../../assets/images/blue-loader.svg'
import Modal from '../Modal'
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
box-shadow: ${({ theme }) => theme.deepShadow};
display: flex;
flex-flow: column nowrap;
margin: 0;
min-height: 600px;
min-width: 375px;
outline: 1px solid ${({ theme }) => theme.backgroundOutline};
padding: 12px;
position: relative;
width: 100%;
`
const ErrorText = styled(ThemedText.BodyPrimary)`
color: ${({ theme }) => theme.accentFailure};
margin: auto !important;
text-align: center;
width: 90%;
`
const StyledIframe = styled.iframe`
background-color: ${({ theme }) => theme.white};
bottom: 0;
left: 0;
height: 100%;
margin: auto;
position: absolute;
right: 0;
top: 0;
width: 100%;
`
const StyledSpinner = styled(CustomLightSpinner)`
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
`
export default function FiatOnrampModal() {
const { account } = useWeb3React()
const theme = useTheme()
const closeModal = useCloseModal(ApplicationModal.FIAT_ONRAMP)
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
const [signedIframeUrl, setSignedIframeUrl] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const fetchSignedIframeUrl = useCallback(async () => {
if (!account) {
setError('Please connect an account before making a purchase.')
return
}
setLoading(true)
setError(null)
try {
const signedIframeUrlFetchEndpoint = process.env.REACT_APP_MOONPAY_LINK as string
const res = await fetch(signedIframeUrlFetchEndpoint, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
colorCode: theme.accentAction,
currencyCode: 'eth',
defaultCurrencyCode: 'eth',
redirectUrl: 'https://app.uniswap.org/#/swap',
walletAddress: account,
}),
})
const { url } = await res.json()
setSignedIframeUrl(url)
} catch (e) {
console.log('there was an error fetching the link', e)
setError(e.toString())
} finally {
setLoading(false)
}
}, [account, theme.accentAction])
useEffect(() => {
fetchSignedIframeUrl()
}, [fetchSignedIframeUrl])
return (
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} minHeight={false} maxHeight={90}>
<Wrapper data-testid="fiat-onramp-modal">
{error ? (
<>
<ThemedText.MediumHeader>
<Trans>Moonpay Fiat On-ramp iframe</Trans>
</ThemedText.MediumHeader>
<ErrorText>
<Trans>something went wrong!</Trans>
<br />
{error}
</ErrorText>
</>
) : loading ? (
<StyledSpinner src={Circle} alt="loading spinner" size="90px" />
) : (
<StyledIframe src={signedIframeUrl ?? ''} frameBorder="0" title="fiat-onramp-iframe" />
)}
</Wrapper>
</Modal>
)
}

@ -0,0 +1,131 @@
export type FiatOnRampWidgetUrlQueryParameters = {
colorCode: string
externalTransactionId: string
}
export type FiatOnRampWidgetUrlQueryResponse = { url: string }
/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#ip_addresses */
export type MoonpayIPAddressesResponse = {
alpha3?: string
isAllowed?: boolean
isBuyAllowed?: boolean
isSellAllowed?: boolean
}
/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#currencies */
type MoonpayCurrency = {
id: string
type: 'crypto' | 'fiat'
name: string
code: string
metadata: {
contractAddress: string
chainId: string
}
}
/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#transactions */
type MoonpayQuote = {
// A positive integer representing how much the customer wants to spend. The minimum amount is 20.
baseCurrencyAmount: number
// A positive integer representing the amount of cryptocurrency the customer will receive. Set when the purchase of cryptocurrency has been executed.
quoteCurrencyAmount: number
quoteCurrencyPrice: number
// A positive integer representing the fee for the transaction. It is added to baseCurrencyAmount, extraFeeAmount and networkFeeAmount when the customer's card is charged.
feeAmount: number
// A positive integer representing your extra fee for the transaction. It is added to baseCurrencyAmount and feeAmount when the customer's card is charged.
extraFeeAmount: number
// A positive integer representing the network fee for the transaction. It is added to baseCurrencyAmount, feeAmount and extraFeeAmount when the customer's card is charged.
networkFeeAmount: number
// A boolean indicating whether baseCurrencyAmount includes or excludes the feeAmount, extraFeeAmount and networkFeeAmount.
areFeesIncluded: boolean
}
/**
* Transaction objects represent cryptocurrency purchases by your end users.
* @ref https://dashboard.moonpay.com/api_reference/client_side_api#transactions
*/
export type MoonpayTransactionsResponse = Array<
MoonpayQuote & {
// Unique identifier for the object.
id: string
// Time at which the object was created. Returned as an ISO 8601 string.
createdAt: string
// Time at which the object was last updated. Returned as an ISO 8601 string.
updatedAt: string
getValidQuote: MoonpayQuote | undefined
baseCurrency: MoonpayCurrency
currency: MoonpayCurrency
// The transaction's status.
status: 'waitingPayment' | 'pending' | 'waitingAuthorization' | 'failed' | 'completed'
// The transaction's failure reason. Set when transaction's status is failed.
failureReason: string
// The cryptocurrency wallet address the purchased funds will be sent to.
walletAddress: string
// The secondary cryptocurrency wallet address identifier for coins such as EOS, XRP and XMR.
walletAddressTag: string
// The cryptocurrency transaction identifier representing the transfer to the customer's wallet. Set when the withdrawal has been executed.
cryptoTransactionId: string
// The URL provided to you, when required, to which to redirect the customer as part of a redirect authentication flow.
redirectUrl: string
// The URL the customer is returned to after they authenticate or cancel their payment on the payment methods app or site. If you are using our widget implementation, this is always our transaction tracker page, which provides the customer with real-time information about their transaction.
returnUrl: string
// The cryptocurrency transaction identifier representing the transfer from the customer's wallet to MoonPay's wallet. Set when the deposit has been executed and received.
depositHash: string
// An optional URL used in a widget implementation. It is passed to us by you in the query parameters, and we include it as a link on the transaction tracker page.
widgetRedirectUrl: string
// The exchange rate between the transaction's base currency and Euro at the time of the transaction.
eurRate: number
// The exchange rate between the transaction's base currency and US Dollar at the time of the transaction.
usdRate: number
// The exchange rate between the transaction's base currency and British Pound at the time of the transaction.
gbpRate: number
// For bank transfer transactions, the information about our bank account to which the customer should make the transfer.
bankDepositInformation: Record<string, unknown>
// For bank transfer transactions, the reference code the customer should cite when making the transfer.
bankTransferReference: string
// The identifier of the cryptocurrency the customer wants to purchase.
currencyId: string
// The identifier of the fiat currency the customer wants to use for the transaction.
baseCurrencyId: string
// The identifier of the customer the transaction is associated with.
customerId: string
// For token or card transactions, the identifier of the payment card used for this transaction.
cardId: string
// For bank transfer transactions, the identifier of the bank account used for this transaction.
bankAccountId: string
// An identifier associated with the customer, provided by you.
externalCustomerId: string
// The transaction's payment method. Possible values are credit_debit_card, sepa_bank_transfer, sepa_open_banking_payment, gbp_bank_transfer, gbp_open_banking_payment, ach_bank_transfer, pix_instant_payment and mobile_wallet
paymentMethod: string
// An identifier associated with the transaction, provided by you.
externalTransactionId: string
// The customer's country. Returned as an ISO 3166-1 alpha-3 code.
country: string
// The customer's state, if the customer is from the USA. Returned as a two-letter code.
state: string
// An array of four objects, each representing one of the four stages of the purchase process. The attributes of each stage are described below.
stages: Array<{
stage: 'stage_one_ordering' | 'stage_two_verification' | 'stage_three_processing' | 'stage_four_delivery'
status: 'not_started' | 'in_progress' | 'success' | 'failed'
failureReason:
| 'card_not_supported'
| 'daily_purchase_limit_exceeded'
| 'payment_authorization_declined'
| 'timeout_3d_secure'
| 'timeout_bank_transfer'
| 'timeout_kyc_verification'
| 'timeout_card_verification'
| 'rejected_kyc'
| 'rejected_card'
| 'rejected_other'
| 'cancelled'
| 'refund'
| 'failed_testnet_withdrawal'
| 'error'
// Sometimes| the customer is required to take an action or actions to further the purchase process| usually by submitting information at a provided URL. For each action| we pass an object with a type and a url.
actions: 'complete_bank_transfer' | 'retry_kyc' | 'verify_card_by_code' | 'verify_card_by_file'
}>
}
>

@ -9,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>`
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
&[data-reach-dialog-overlay] {
z-index: ${Z_INDEX.modalBackdrop};
background-color: transparent;
@ -17,7 +17,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
display: flex;
align-items: center;
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
justify-content: center;
background-color: ${({ theme }) => theme.backgroundScrim};
@ -89,7 +89,7 @@ interface ModalProps {
maxWidth?: number
initialFocusRef?: React.RefObject<any>
children?: React.ReactNode
scrollOverlay?: boolean
$scrollOverlay?: boolean
hideBorder?: boolean
isBottomSheet?: boolean
}
@ -103,7 +103,7 @@ export default function Modal({
initialFocusRef,
children,
onSwipe = onDismiss,
scrollOverlay,
$scrollOverlay,
isBottomSheet = isMobile,
hideBorder = false,
}: ModalProps) {
@ -136,7 +136,7 @@ export default function Modal({
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
unstable_lockFocusAcrossFrames={false}
scrollOverlay={scrollOverlay}
$scrollOverlay={$scrollOverlay}
>
<StyledDialogContent
{...(isMobile
@ -149,7 +149,7 @@ export default function Modal({
$minHeight={minHeight}
$maxHeight={maxHeight}
$isBottomSheet={isBottomSheet}
$scrollOverlay={scrollOverlay}
$scrollOverlay={$scrollOverlay}
$hideBorder={hideBorder}
$maxWidth={maxWidth}
>

@ -1,6 +1,9 @@
import { useWeb3React } from '@web3-react/core'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import FiatOnrampModal from 'components/FiatOnrampModal'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import NftExploreBanner from 'nft/components/nftExploreBanner/NftExploreBanner'
import { lazy } from 'react'
@ -18,20 +21,22 @@ export default function TopLevelModals() {
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React()
const location = useLocation()
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
const pageShowsNftPromoBanner =
location.pathname.startsWith('/swap') ||
location.pathname.startsWith('/tokens') ||
location.pathname.startsWith('/pool')
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
const accountBlocked = Boolean(blockedAccountModalOpen && account)
return (
<>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} />
<ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
<Bag />
<TransactionCompleteModal />
<AirdropModal />
{pageShowsNftPromoBanner && <NftExploreBanner />}
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
</>
)
}

@ -348,7 +348,7 @@ export default function TransactionConfirmationModal({
// confirmation screen
return (
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
<Modal isOpen={isOpen} $scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
) : attemptingTxn ? (

@ -23,7 +23,7 @@ import styled, { css } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { shortenAddress } from '../../nft/utils/address'
import { useCloseModal, useToggleModal } from '../../state/application/hooks'
import { useCloseModal, useOpenModal, useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import { ButtonEmphasis, ButtonSize, ThemeButton } from '../Button'
@ -143,7 +143,6 @@ const AuthenticatedHeader = () => {
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const navigate = useNavigate()
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
const fiatOnrampFlag = useFiatOnrampFlag()
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
const resetSellAssets = useSellAsset((state) => state.reset)
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
@ -170,13 +169,16 @@ const AuthenticatedHeader = () => {
return price * balance
}, [balanceString, nativeCurrencyPrice])
const navigateToProfile = () => {
const navigateToProfile = useCallback(() => {
resetSellAssets()
setSellPageState(ProfilePageStateType.VIEWING)
clearCollectionFilters()
navigate('/nfts/profile')
closeModal()
}
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
const fiatOnrampFlag = useFiatOnrampFlag()
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
return (
<AuthenticatedHeaderWrapper>
@ -222,7 +224,7 @@ const AuthenticatedHeader = () => {
<Trans>View and sell NFTs</Trans>
</ProfileButton>
{fiatOnrampFlag === BaseVariant.Enabled && (
<BuyCryptoButton size={ButtonSize.medium} emphasis={ButtonEmphasis.medium} onClick={openNftModal}>
<BuyCryptoButton size={ButtonSize.medium} emphasis={ButtonEmphasis.medium} onClick={openFiatOnrampModal}>
<CreditCard /> <Trans>Buy crypto</Trans>
</BuyCryptoButton>
)}

@ -15,25 +15,26 @@ export type PopupContent =
export enum ApplicationModal {
ADDRESS_CLAIM,
UNISWAP_NFT_AIRDROP_CLAIM,
BLOCKED_ACCOUNT,
DELEGATE,
CLAIM_POPUP,
DELEGATE,
EXECUTE,
FEATURE_FLAGS,
FIAT_ONRAMP,
MENU,
NETWORK_FILTER,
NETWORK_SELECTOR,
POOL_OVERVIEW_OPTIONS,
PRIVACY_POLICY,
QUEUE,
SELF_CLAIM,
SETTINGS,
SHARE,
TIME_SELECTOR,
VOTE,
WALLET,
WALLET_DROPDOWN,
QUEUE,
EXECUTE,
TIME_SELECTOR,
SHARE,
NETWORK_FILTER,
FEATURE_FLAGS,
UNISWAP_NFT_AIRDROP_CLAIM,
}
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>