mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 09:23:21 +01:00
Add a new fiat onboarding option via MoonPay (#13934)
This commit is contained in:
parent
64d35458b0
commit
95f830438a
10
app/_locales/en/messages.json
generated
10
app/_locales/en/messages.json
generated
@ -411,6 +411,13 @@
|
||||
"buy": {
|
||||
"message": "Buy"
|
||||
},
|
||||
"buyCryptoWithMoonPay": {
|
||||
"message": "Buy $1 with MoonPay",
|
||||
"description": "$1 represents the cypto symbol to be purchased"
|
||||
},
|
||||
"buyCryptoWithMoonPayDescription": {
|
||||
"message": "MoonPay supports popular payment methods, including Visa, Mastercard, Apple / Google / Samsung Pay, and bank transfers in 146+ countries. Tokens deposit into your MetaMask account."
|
||||
},
|
||||
"buyCryptoWithTransak": {
|
||||
"message": "Buy $1 with Transak",
|
||||
"description": "$1 represents the cypto symbol to be purchased"
|
||||
@ -617,6 +624,9 @@
|
||||
"continue": {
|
||||
"message": "Continue"
|
||||
},
|
||||
"continueToMoonPay": {
|
||||
"message": "Continue to MoonPay"
|
||||
},
|
||||
"continueToTransak": {
|
||||
"message": "Continue to Transak"
|
||||
},
|
||||
|
@ -1 +1,2 @@
|
||||
export const TRANSAK_API_KEY = '25ac1309-a49b-4411-b20e-5e56c61a5b1c'; // It's a public key, which will be included in a URL for Transak.
|
||||
export const MOONPAY_API_KEY = 'pk_live_WbCpe6PxSIcGPCSd6lKCbJNRht7uy'; // Publishable key.
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
} from '../../../shared/constants/network';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
|
||||
import { TRANSAK_API_KEY } from '../constants/on-ramp';
|
||||
import { TRANSAK_API_KEY, MOONPAY_API_KEY } from '../constants/on-ramp';
|
||||
|
||||
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
|
||||
|
||||
@ -67,6 +67,47 @@ const createTransakUrl = (walletAddress, chainId) => {
|
||||
return `https://global.transak.com/?${queryParams}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a MoonPay Checkout URL.
|
||||
*
|
||||
* @param {string} walletAddress - Destination address
|
||||
* @param {string} chainId - Current chain ID
|
||||
* @returns String
|
||||
*/
|
||||
const createMoonPayUrl = async (walletAddress, chainId) => {
|
||||
const {
|
||||
moonPay: { defaultCurrencyCode, showOnlyCurrencies } = {},
|
||||
} = BUYABLE_CHAINS_MAP[chainId];
|
||||
const moonPayQueryParams = new URLSearchParams({
|
||||
apiKey: MOONPAY_API_KEY,
|
||||
walletAddress,
|
||||
defaultCurrencyCode,
|
||||
showOnlyCurrencies,
|
||||
});
|
||||
const queryParams = new URLSearchParams({
|
||||
url: `https://buy.moonpay.com?${moonPayQueryParams}`,
|
||||
context: 'extension',
|
||||
});
|
||||
const moonPaySignUrl = `${SWAPS_API_V2_BASE_URL}/moonpaySign/?${queryParams}`;
|
||||
try {
|
||||
const response = await fetchWithTimeout(moonPaySignUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const parsedResponse = await response.json();
|
||||
if (response.ok && parsedResponse.url) {
|
||||
return parsedResponse.url;
|
||||
}
|
||||
log.warn('Failed to create a MoonPay purchase URL', parsedResponse);
|
||||
} catch (err) {
|
||||
log.warn('Failed to create a MoonPay purchase URL', err);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Gives the caller a url at which the user can acquire eth, depending on the network they are in
|
||||
*
|
||||
@ -89,6 +130,8 @@ export default async function getBuyUrl({ chainId, address, service }) {
|
||||
return await createWyrePurchaseUrl(address);
|
||||
case 'transak':
|
||||
return createTransakUrl(address, chainId);
|
||||
case 'moonpay':
|
||||
return createMoonPayUrl(address, chainId);
|
||||
case 'metamask-faucet':
|
||||
return 'https://faucet.metamask.io/';
|
||||
case 'rinkeby-faucet':
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
ETH_SYMBOL,
|
||||
BUYABLE_CHAINS_MAP,
|
||||
} from '../../../shared/constants/network';
|
||||
import { TRANSAK_API_KEY } from '../constants/on-ramp';
|
||||
import { TRANSAK_API_KEY, MOONPAY_API_KEY } from '../constants/on-ramp';
|
||||
import { SWAPS_API_V2_BASE_URL } from '../../../shared/constants/swaps';
|
||||
import getBuyUrl from './buy-url';
|
||||
|
||||
@ -114,4 +114,35 @@ describe('buy-url', () => {
|
||||
const kovanUrl = await getBuyUrl(KOVAN);
|
||||
expect(kovanUrl).toStrictEqual('https://github.com/kovan-testnet/faucet');
|
||||
});
|
||||
|
||||
it('returns a MoonPay url with a prefilled wallet address for the Ethereum network', async () => {
|
||||
const {
|
||||
moonPay: { defaultCurrencyCode, showOnlyCurrencies } = {},
|
||||
} = BUYABLE_CHAINS_MAP[MAINNET.chainId];
|
||||
const moonPayQueryParams = new URLSearchParams({
|
||||
apiKey: MOONPAY_API_KEY,
|
||||
walletAddress: MAINNET.address,
|
||||
defaultCurrencyCode,
|
||||
showOnlyCurrencies,
|
||||
});
|
||||
const queryParams = new URLSearchParams({
|
||||
url: `https://buy.moonpay.com?${moonPayQueryParams}`,
|
||||
context: 'extension',
|
||||
});
|
||||
nock(SWAPS_API_V2_BASE_URL)
|
||||
.get(`/moonpaySign/?${queryParams}`)
|
||||
.reply(200, {
|
||||
url: `https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=eth%2Cusdt%2Cusdc%2Cdai&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`,
|
||||
});
|
||||
const moonPayUrl = await getBuyUrl({ ...MAINNET, service: 'moonpay' });
|
||||
expect(moonPayUrl).toStrictEqual(
|
||||
`https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=eth%2Cusdt%2Cusdc%2Cdai&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`,
|
||||
);
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('returns an empty string if generating a MoonPay url fails', async () => {
|
||||
const moonPayUrl = await getBuyUrl({ ...MAINNET, service: 'moonpay' });
|
||||
expect(moonPayUrl).toStrictEqual('');
|
||||
});
|
||||
});
|
||||
|
@ -185,11 +185,16 @@ export const IPFS_DEFAULT_GATEWAY_URL = 'dweb.link';
|
||||
// The first item in transakCurrencies must be the
|
||||
// default crypto currency for the network
|
||||
const BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME = 'ethereum';
|
||||
|
||||
export const BUYABLE_CHAINS_MAP = {
|
||||
[MAINNET_CHAIN_ID]: {
|
||||
nativeCurrency: ETH_SYMBOL,
|
||||
network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME,
|
||||
transakCurrencies: [ETH_SYMBOL, 'USDT', 'USDC', 'DAI'],
|
||||
moonPay: {
|
||||
defaultCurrencyCode: 'eth',
|
||||
showOnlyCurrencies: 'eth,usdt,usdc,dai',
|
||||
},
|
||||
},
|
||||
[ROPSTEN_CHAIN_ID]: {
|
||||
nativeCurrency: ETH_SYMBOL,
|
||||
@ -211,16 +216,28 @@ export const BUYABLE_CHAINS_MAP = {
|
||||
nativeCurrency: BNB_SYMBOL,
|
||||
network: 'bsc',
|
||||
transakCurrencies: [BNB_SYMBOL, 'BUSD'],
|
||||
moonPay: {
|
||||
defaultCurrencyCode: 'bnb_bsc',
|
||||
showOnlyCurrencies: 'bnb_bsc,busd_bsc',
|
||||
},
|
||||
},
|
||||
[POLYGON_CHAIN_ID]: {
|
||||
nativeCurrency: MATIC_SYMBOL,
|
||||
network: 'polygon',
|
||||
transakCurrencies: [MATIC_SYMBOL, 'USDT', 'USDC', 'DAI'],
|
||||
moonPay: {
|
||||
defaultCurrencyCode: 'matic_polygon',
|
||||
showOnlyCurrencies: 'matic_polygon,usdc_polygon',
|
||||
},
|
||||
},
|
||||
[AVALANCHE_CHAIN_ID]: {
|
||||
nativeCurrency: AVALANCHE_SYMBOL,
|
||||
network: 'avaxcchain',
|
||||
transakCurrencies: [AVALANCHE_SYMBOL],
|
||||
moonPay: {
|
||||
defaultCurrencyCode: 'avax_cchain',
|
||||
showOnlyCurrencies: 'avax_cchain',
|
||||
},
|
||||
},
|
||||
[FANTOM_CHAIN_ID]: {
|
||||
nativeCurrency: FANTOM_SYMBOL,
|
||||
@ -231,5 +248,9 @@ export const BUYABLE_CHAINS_MAP = {
|
||||
nativeCurrency: CELO_SYMBOL,
|
||||
network: 'celo',
|
||||
transakCurrencies: [CELO_SYMBOL],
|
||||
moonPay: {
|
||||
defaultCurrencyCode: 'celo',
|
||||
showOnlyCurrencies: 'celo',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -101,7 +101,7 @@ export const WAVAX_CONTRACT_ADDRESS =
|
||||
|
||||
const SWAPS_TESTNET_CHAIN_ID = '0x539';
|
||||
|
||||
export const SWAPS_API_V2_BASE_URL = 'https://api2.metaswap.codefi.network';
|
||||
export const SWAPS_API_V2_BASE_URL = 'https://swap.metaswap.codefi.network';
|
||||
export const SWAPS_DEV_API_V2_BASE_URL =
|
||||
'https://swap.metaswap-dev.codefi.network';
|
||||
export const GAS_API_BASE_URL = 'https://gas-api.metaswap.codefi.network';
|
||||
|
@ -48,7 +48,7 @@ async function setupFetchMocking(driver) {
|
||||
return { json: async () => clone(mockResponses.gasPricesBasic) };
|
||||
} else if (url.match(/chromeextensionmm/u)) {
|
||||
return { json: async () => clone(mockResponses.metametrics) };
|
||||
} else if (url.match(/^https:\/\/(api2\.metaswap\.codefi\.network)/u)) {
|
||||
} else if (url.match(/^https:\/\/(swap\.metaswap\.codefi\.network)/u)) {
|
||||
if (url.match(/featureFlags$/u)) {
|
||||
return { json: async () => clone(mockResponses.swaps.featureFlags) };
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
export const METASWAP_BASE_URL = 'https://api2.metaswap.codefi.network';
|
||||
export const METASWAP_BASE_URL = 'https://swap.metaswap.codefi.network';
|
||||
export const GAS_API_URL = 'https://gas-api.metaswap.codefi.network';
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
BUYABLE_CHAINS_MAP,
|
||||
} from '../../../../../shared/constants/network';
|
||||
import Button from '../../../ui/button';
|
||||
import LogoMoonPay from '../../../ui/logo/logo-moonpay';
|
||||
|
||||
export default class DepositEtherModal extends Component {
|
||||
static contextTypes = {
|
||||
@ -17,8 +18,10 @@ export default class DepositEtherModal extends Component {
|
||||
isTestnet: PropTypes.bool.isRequired,
|
||||
isMainnet: PropTypes.bool.isRequired,
|
||||
isBuyableTransakChain: PropTypes.bool.isRequired,
|
||||
isBuyableMoonPayChain: PropTypes.bool.isRequired,
|
||||
toWyre: PropTypes.func.isRequired,
|
||||
toTransak: PropTypes.func.isRequired,
|
||||
toMoonPay: PropTypes.func.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
toFaucet: PropTypes.func.isRequired,
|
||||
hideWarning: PropTypes.func.isRequired,
|
||||
@ -93,11 +96,13 @@ export default class DepositEtherModal extends Component {
|
||||
chainId,
|
||||
toWyre,
|
||||
toTransak,
|
||||
toMoonPay,
|
||||
address,
|
||||
toFaucet,
|
||||
isTestnet,
|
||||
isMainnet,
|
||||
isBuyableTransakChain,
|
||||
isBuyableMoonPayChain,
|
||||
} = this.props;
|
||||
const { t } = this.context;
|
||||
const networkName = NETWORK_TO_NAME_MAP[chainId];
|
||||
@ -122,31 +127,6 @@ export default class DepositEtherModal extends Component {
|
||||
</div>
|
||||
<div className="page-container__content">
|
||||
<div className="deposit-ether-modal__buy-rows">
|
||||
{this.renderRow({
|
||||
logo: (
|
||||
<div
|
||||
className="deposit-ether-modal__logo"
|
||||
style={{
|
||||
backgroundImage: "url('./images/wyre.svg')",
|
||||
height: '40px',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
title: t('buyWithWyre'),
|
||||
text: t('buyWithWyreDescription'),
|
||||
buttonLabel: t('continueToWyre'),
|
||||
onButtonClick: () => {
|
||||
this.context.metricsEvent({
|
||||
eventOpts: {
|
||||
category: 'Accounts',
|
||||
action: 'Deposit Ether',
|
||||
name: 'Click buy Ether via Wyre',
|
||||
},
|
||||
});
|
||||
toWyre(address);
|
||||
},
|
||||
hide: !isMainnet,
|
||||
})}
|
||||
{this.renderRow({
|
||||
logo: (
|
||||
<div
|
||||
@ -172,6 +152,50 @@ export default class DepositEtherModal extends Component {
|
||||
},
|
||||
hide: !isBuyableTransakChain,
|
||||
})}
|
||||
{this.renderRow({
|
||||
logo: (
|
||||
<LogoMoonPay className="deposit-ether-modal__logo--moonpay" />
|
||||
),
|
||||
title: t('buyCryptoWithMoonPay', [symbol]),
|
||||
text: t('buyCryptoWithMoonPayDescription', [symbol]),
|
||||
buttonLabel: t('continueToMoonPay'),
|
||||
onButtonClick: () => {
|
||||
this.context.metricsEvent({
|
||||
eventOpts: {
|
||||
category: 'Accounts',
|
||||
action: 'Deposit tokens',
|
||||
name: 'Click buy tokens via MoonPay',
|
||||
},
|
||||
});
|
||||
toMoonPay(address, chainId);
|
||||
},
|
||||
hide: !isBuyableMoonPayChain,
|
||||
})}
|
||||
{this.renderRow({
|
||||
logo: (
|
||||
<div
|
||||
className="deposit-ether-modal__logo"
|
||||
style={{
|
||||
backgroundImage: "url('./images/wyre.svg')",
|
||||
height: '40px',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
title: t('buyWithWyre'),
|
||||
text: t('buyWithWyreDescription'),
|
||||
buttonLabel: t('continueToWyre'),
|
||||
onButtonClick: () => {
|
||||
this.context.metricsEvent({
|
||||
eventOpts: {
|
||||
category: 'Accounts',
|
||||
action: 'Deposit Ether',
|
||||
name: 'Click buy Ether via Wyre',
|
||||
},
|
||||
});
|
||||
toWyre(address);
|
||||
},
|
||||
hide: !isMainnet,
|
||||
})}
|
||||
{this.renderRow({
|
||||
logo: (
|
||||
<img
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
getCurrentChainId,
|
||||
getSelectedAddress,
|
||||
getIsBuyableTransakChain,
|
||||
getIsBuyableMoonPayChain,
|
||||
} from '../../../../selectors/selectors';
|
||||
import DepositEtherModal from './deposit-ether-modal.component';
|
||||
|
||||
@ -21,6 +22,7 @@ function mapStateToProps(state) {
|
||||
isMainnet: getIsMainnet(state),
|
||||
address: getSelectedAddress(state),
|
||||
isBuyableTransakChain: getIsBuyableTransakChain(state),
|
||||
isBuyableMoonPayChain: getIsBuyableMoonPayChain(state),
|
||||
};
|
||||
}
|
||||
|
||||
@ -32,6 +34,9 @@ function mapDispatchToProps(dispatch) {
|
||||
toTransak: (address, chainId) => {
|
||||
dispatch(buyEth({ service: 'transak', address, chainId }));
|
||||
},
|
||||
toMoonPay: (address, chainId) => {
|
||||
dispatch(buyEth({ service: 'moonpay', address, chainId }));
|
||||
},
|
||||
hideModal: () => {
|
||||
dispatch(hideModal());
|
||||
},
|
||||
|
@ -26,6 +26,10 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&--moonpay {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&__buy-row {
|
||||
|
File diff suppressed because one or more lines are too long
45
ui/components/ui/logo/logo-moonpay.js
Normal file
45
ui/components/ui/logo/logo-moonpay.js
Normal file
File diff suppressed because one or more lines are too long
11
ui/components/ui/logo/logo-moonpay.test.js
Normal file
11
ui/components/ui/logo/logo-moonpay.test.js
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { renderWithProvider } from '../../../../test/jest';
|
||||
import LogoMoonPay from './logo-moonpay';
|
||||
|
||||
describe('LogoMoonPay', () => {
|
||||
it('renders the LogoMoonPay component', () => {
|
||||
const { container } = renderWithProvider(<LogoMoonPay />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -34,7 +34,7 @@ describe('Ducks - Swaps', () => {
|
||||
describe('fetchSwapsLivenessAndFeatureFlags', () => {
|
||||
const cleanFeatureFlagApiCache = () => {
|
||||
setStorageItem(
|
||||
'cachedFetch:https://api2.metaswap.codefi.network/featureFlags',
|
||||
'cachedFetch:https://swap.metaswap.codefi.network/featureFlags',
|
||||
null,
|
||||
);
|
||||
};
|
||||
@ -47,7 +47,7 @@ describe('Ducks - Swaps', () => {
|
||||
featureFlagsResponse,
|
||||
replyWithError = false,
|
||||
} = {}) => {
|
||||
const apiNock = nock('https://api2.metaswap.codefi.network').get(
|
||||
const apiNock = nock('https://swap.metaswap.codefi.network').get(
|
||||
'/featureFlags',
|
||||
);
|
||||
if (replyWithError) {
|
||||
|
@ -94,7 +94,7 @@ describe('Swaps Util', () => {
|
||||
},
|
||||
};
|
||||
it('should fetch trade info on prod', async () => {
|
||||
nock('https://api2.metaswap.codefi.network')
|
||||
nock('https://swap.metaswap.codefi.network')
|
||||
.get('/networks/1/trades')
|
||||
.query(true)
|
||||
.reply(200, MOCK_TRADE_RESPONSE_2);
|
||||
@ -120,7 +120,7 @@ describe('Swaps Util', () => {
|
||||
|
||||
describe('fetchTokens', () => {
|
||||
beforeAll(() => {
|
||||
nock('https://api2.metaswap.codefi.network')
|
||||
nock('https://swap.metaswap.codefi.network')
|
||||
.persist()
|
||||
.get('/networks/1/tokens')
|
||||
.reply(200, TOKENS);
|
||||
@ -139,7 +139,7 @@ describe('Swaps Util', () => {
|
||||
|
||||
describe('fetchAggregatorMetadata', () => {
|
||||
beforeAll(() => {
|
||||
nock('https://api2.metaswap.codefi.network')
|
||||
nock('https://swap.metaswap.codefi.network')
|
||||
.persist()
|
||||
.get('/networks/1/aggregatorMetadata')
|
||||
.reply(200, AGGREGATOR_METADATA);
|
||||
@ -158,7 +158,7 @@ describe('Swaps Util', () => {
|
||||
|
||||
describe('fetchTopAssets', () => {
|
||||
beforeAll(() => {
|
||||
nock('https://api2.metaswap.codefi.network')
|
||||
nock('https://swap.metaswap.codefi.network')
|
||||
.persist()
|
||||
.get('/networks/1/topAssets')
|
||||
.reply(200, TOP_ASSETS);
|
||||
|
@ -672,6 +672,11 @@ export function getIsBuyableTransakChain(state) {
|
||||
return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.transakCurrencies);
|
||||
}
|
||||
|
||||
export function getIsBuyableMoonPayChain(state) {
|
||||
const chainId = getCurrentChainId(state);
|
||||
return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.moonPay);
|
||||
}
|
||||
|
||||
export function getNativeCurrencyImage(state) {
|
||||
const nativeCurrency = getNativeCurrency(state).toUpperCase();
|
||||
return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency];
|
||||
|
@ -2102,10 +2102,12 @@ export function showSendTokenPage() {
|
||||
export function buyEth(opts) {
|
||||
return async (dispatch) => {
|
||||
const url = await getBuyUrl(opts);
|
||||
global.platform.openTab({ url });
|
||||
dispatch({
|
||||
type: actionConstants.BUY_ETH,
|
||||
});
|
||||
if (url) {
|
||||
global.platform.openTab({ url });
|
||||
dispatch({
|
||||
type: actionConstants.BUY_ETH,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user