diff --git a/src/appConfig/_default.json b/src/appConfig/_default.json index 06a7936c..32fc8241 100644 --- a/src/appConfig/_default.json +++ b/src/appConfig/_default.json @@ -15,11 +15,13 @@ "argent": "", "argentWebWallet": "", "avnu": "", + "banxa": "", "influence": "", "influenceImage": "", "ipfs": "", "ramp": "", "ClientId": { + "banxa": "", "google": "", "gtm": "", "layerswap": "", @@ -27,6 +29,9 @@ "stripe": "" } }, + "Banxa": { + "minFiat": 11 + }, "Cloudfront": { "imageUrl": "", "otherUrl": "", diff --git a/src/appConfig/prerelease.json b/src/appConfig/prerelease.json index af92811b..cbb1bf8f 100644 --- a/src/appConfig/prerelease.json +++ b/src/appConfig/prerelease.json @@ -7,10 +7,10 @@ "Api": { "argentWebWallet": "https://sepolia-web.ready.co", "avnu": "https://sepolia.api.avnu.fi", + "banxa": "https://api.banxa.com", "influence": "https://api-prerelease.influenceth.io", "influenceImage": "https://images-prerelease.influenceth.io", - "ipfs": "https://influence.infura-ipfs.io/ipfs", - "ramp": "https://app.demo.ramp.network" + "ipfs": "https://influence.infura-ipfs.io/ipfs" }, "Cloudfront": { "imageUrl": "https://d2xo5vocah3zyk.cloudfront.net", diff --git a/src/appConfig/production.json b/src/appConfig/production.json index 7209a72e..af8db302 100644 --- a/src/appConfig/production.json +++ b/src/appConfig/production.json @@ -6,10 +6,10 @@ "Api": { "argentWebWallet": "https://web.ready.co", "avnu": "https://starknet.api.avnu.fi", + "banxa": "https://api.banxa.com", "influence": "https://api.influenceth.io", "influenceImage": "https://images.influenceth.io", - "ipfs": "https://influence.infura-ipfs.io/ipfs", - "ramp": "https://app.ramp.network" + "ipfs": "https://influence.infura-ipfs.io/ipfs" }, "Cloudfront": { "imageUrl": "https://d2xo5vocah3zyk.cloudfront.net", diff --git a/src/game/launcher/store/FundingFlow.js b/src/game/launcher/store/FundingFlow.js index e07caf23..87a42f68 100644 --- a/src/game/launcher/store/FundingFlow.js +++ b/src/game/launcher/store/FundingFlow.js @@ -1,23 +1,23 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { createPortal } from 'react-dom'; -import { PropagateLoader as Loader } from 'react-spinners'; -import { RampInstantSDK } from '@ramp-network/ramp-instant-sdk'; +import { PropagateLoader as Loader, PuffLoader as AltLoader } from 'react-spinners'; import { appConfig } from '~/appConfig'; import Button from '~/components/ButtonAlt'; -import { ChevronRightIcon, CloseIcon, LinkIcon, WalletIcon } from '~/components/Icons'; +import { ChevronRightIcon, CloseIcon, WalletIcon } from '~/components/Icons'; import Details from '~/components/DetailsV2'; import useSession from '~/hooks/useSession'; import BrightButton from '~/components/BrightButton'; -import MouseoverInfoPane from '~/components/MouseoverInfoPane'; import useWalletPurchasableBalances from '~/hooks/useWalletPurchasableBalances'; import UserPrice from '~/components/UserPrice'; -import { TOKEN, TOKEN_FORMAT, TOKEN_FORMATTER } from '~/lib/priceUtils'; +import { TOKEN, TOKEN_FORMAT, TOKEN_FORMATTER, TOKEN_SCALE } from '~/lib/priceUtils'; import usePriceHelper from '~/hooks/usePriceHelper'; import useStore from '~/hooks/useStore'; import EthFaucetButton from './components/EthFaucetButton'; import { areChainsEqual, fireTrackingEvent, resolveChainId, safeBigInt } from '~/lib/utils'; +import api from '~/lib/api'; +import PageLoader from '~/components/PageLoader'; const layerSwapChains = { SN_MAIN: { ethereum: 'ETHEREUM_MAINNET', starknet: 'STARKNET_MAINNET' }, @@ -227,77 +227,14 @@ const WaitingWrapper = styled.div` } `; -const RampWrapper = styled.div` - background: linear-gradient(225deg, black, rgba(${p => p.theme.colors.mainRGB}, 0.3)); - ${p => !p.display && ` - height: 0; - overflow: hidden; - width: 0; - `} - & > div { - height: 600px; - width: 900px; - } +const LoaderWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 400px; + width: 400px; `; -const RAMP_PURCHASE_STATUS = { - INITIALIZED: { - statusText: 'The purchase has been initialized.', - isSuccess: false, - isError: false - }, - PAYMENT_STARTED: { - statusText: 'Automated payment has been initiated.', - isSuccess: false, - isError: false - }, - PAYMENT_IN_PROGRESS: { - statusText: 'Payment process has been completed.', - isSuccess: false, - isError: false - }, - PAYMENT_FAILED: { - statusText: 'The payment was cancelled, rejected, or otherwise failed.', - isSuccess: false, - isError: true - }, - PAYMENT_EXECUTED: { - statusText: 'Payment approved, waiting for funds to be received.', - isSuccess: false, - isError: false - }, - FIAT_SENT: { - statusText: 'Outgoing bank transfer has been confirmed.', - isSuccess: false, - isError: false - }, - FIAT_RECEIVED: { - statusText: 'Payment confirmed, final checks before crypto transfer.', - isSuccess: false, - isError: false - }, - RELEASING: { - statusText: 'Funds received, initiating crypto transfer...', - isSuccess: false, - isError: false - }, - RELEASED: { - statusText: 'Waiting for funds to be received by user\'s wallet...', - isSuccess: true, - isError: false - }, - EXPIRED: { - statusText: 'Time to pay for the purchase was exceeded. Please try again, making sure to follow all prompts.', - isSuccess: false, - isError: true - }, - CANCELLED: { - statusText: 'The purchase was been cancelled.', - isSuccess: false, - isError: true - } -}; - export const FundingFlow = ({ totalPrice, onClose, onFunded }) => { const createAlert = useStore(s => s.dispatchAlertLogged); @@ -306,8 +243,7 @@ export const FundingFlow = ({ totalPrice, onClose, onFunded }) => { const { data: wallet, refetch: refetchBalances } = useWalletPurchasableBalances(); const preferredUiCurrency = useStore(s => s.getPreferredUiCurrency()); - const [hoveredRampButton, setHoveredRampButton] = useState(false); - const [ramping, setRamping] = useState(); + const [banxaing, setBanxaing] = useState(); const [waiting, setWaiting] = useState(); const startingBalance = useRef(); @@ -319,7 +255,7 @@ export const FundingFlow = ({ totalPrice, onClose, onFunded }) => { // if (waiting && !debug) { // setTimeout(() => { // console.log('hack', startingBalance.current, wallet.tokenBalances); // tokenBalances - // startingBalance.current[TOKEN.ETH] -= safeBigInt(1e14); + // startingBalance.current[TOKEN.USDC] -= safeBigInt(TOKEN_SCALE[TOKEN.USDC]); // setDebug(1); // }, 5000); // } @@ -393,82 +329,60 @@ export const FundingFlow = ({ totalPrice, onClose, onFunded }) => { return [needed] }, [fundsNeeded]); - const to = useRef(); - const onRampHover = useCallback((which) => (e) => { - if (to.current) clearTimeout(to.current); - if (which) { - setHoveredRampButton(e.target); - } else { // close on delay so have time to click the link - to.current = setTimeout(() => { - setHoveredRampButton(); - }, 1500); - } - }, []); + const [banxaOrder, setBanxaOrder] = useState({}); - const [rampPurchase, setRampPurchase] = useState(); - const checkRampPurchase = useCallback(async (purchase) => { - try { - const response = await fetch( - `${appConfig.get('Api.ramp')}/api/host-api/purchase/${purchase.id}?secret=${purchase.purchaseViewToken}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - } - } - ); - if (response.ok) { - const updatePurchaseObject = await response.json(); - setRampPurchase(updatePurchaseObject); - - // stop checking if terminal status - const status = RAMP_PURCHASE_STATUS[updatePurchaseObject.status]; - if (status.isError || status.isSuccess) { - return; - } - } else { - console.error('Response not ok:', response); - } - } catch (error) { - console.error('Error fetching purchase info:', error); + const checkBanxaOrder = useCallback(async (purchase) => { + console.log('Checking Banxa order', purchase); + if (!purchase?.id) return; + + const order = await api.getBanxaOrder(purchase.id); + if (order?.id) { + setBanxaOrder((o) => ({ ...o, order })); } }, []); + useEffect(() => { - if (rampPurchase) { - const i = setInterval(() => { checkRampPurchase(rampPurchase); }, 5000); + if (banxaOrder?.id) { + const i = setInterval(() => { checkBanxaOrder(banxaOrder); }, 5000); + // (part of debug) + // const i = setInterval(() => { + // setBanxaOrder(); + // setBanxaing(); + // setWaiting(true); + // }, 5000); return () => clearInterval(i); } - }, [checkRampPurchase, rampPurchase]) - - const onClickCC = useCallback((amount) => () => { + }, [checkBanxaOrder, banxaOrder]); + + const onClickCC = useCallback((amount) => async () => { fireTrackingEvent('funding_start', { externalId: accountAddress }); - setRamping(true); - setRampPurchase(); - - setTimeout(() => { - const embeddedRamp = new RampInstantSDK({ - hostAppName: 'Influence', - hostLogoUrl: window.location.origin + '/maskable-logo-192x192.png', - hostApiKey: appConfig.get('Api.ClientId.ramp'), - userAddress: accountAddress, - swapAsset: 'STARKNET_ETH', // TODO: STARKNET_USDC once enabled - fiatCurrency: 'USD', - fiatValue: Math.ceil(amount / 1e6), - url: appConfig.get('Api.ramp'), - - variant: 'embedded-desktop', - containerNode: document.getElementById('ramp-container') - }) - embeddedRamp.on('PURCHASE_CREATED', (e) => { - console.log('PURCHASE_CREATED', e); - try { - setRampPurchase(e.payload.purchase); - } catch (e) { - console.warn('purchase_created event missing payload!', e); - } + + try { + setBanxaing(true); + + const order = await api.createBanxaOrder({ + // TODO: can alternatively support an amount in crypto here too + usd: Math.max( // <-- this is the fiat amount (fees deducted mean will result in less USDC than this) + appConfig.get('Banxa.minFiat'), + Math.ceil(amount / TOKEN_SCALE[TOKEN.USDC]) + ), + crypto: 'USDC' }); - embeddedRamp.show(); - }, 100); + if (!order?.checkoutUrl) throw new Error('Banxa order creation returned empty'); + + setBanxaOrder(order); + } catch (error) { + createAlert({ + type: 'GenericAlert', + data: { content: 'Error initiating checkout flow.' }, + level: 'warning', + duration: 5000 + }); + + console.error('Error fetching Banxa checkout URL:', error); + fireTrackingEvent('funding_error', { externalId: accountAddress }); + setBanxaing(); + } }, [accountAddress]); const [layerswapUrl, setLayerswapUrl] = useState(); @@ -514,50 +428,61 @@ export const FundingFlow = ({ totalPrice, onClose, onFunded }) => { onClose(); }, [onClose]); - useEffect(() => { - // error: clear ramp purchase + close funding dialog - if (RAMP_PURCHASE_STATUS[rampPurchase?.status]?.isError) { - // fire error - fireTrackingEvent('funding_error', { externalId: accountAddress, status: rampPurchase?.status }); + const banxaOrderStatusMessage = useMemo(() => { + switch (banxaOrder?.status) { + case 'pendingPayment': return 'Awaiting payment...'; + case 'waitingPayment': return 'Processing payment...'; + case 'paymentReceived': return 'Payment received, processing...'; + case 'inProgress': return 'Final verification, processing...'; + case 'cryptoTransferred': return 'Crypto transfer initiated...'; + + case 'cancelled': return 'Order has been cancelled by Banxa due to internal risk and compliance alerts.'; + case 'declined': return 'Payment method declined.'; + case 'expired': return 'Checkout has expired. Please start over.'; + }; + }, [banxaOrder?.status]); + + const hasTrackedExecution = useRef(false); + const showSlowWarning = useMemo(() => ( + ['paymentReceived', 'inProgress', 'cryptoTransferred'].includes(banxaOrder?.status) + ), [banxaOrder?.status]); - // alert user - createAlert({ - type: 'GenericAlert', - data: { content: <>RAMP PAYMENT ERROR: "{RAMP_PURCHASE_STATUS[rampPurchase?.status].statusText}"

Click for more information. }, - level: 'warning', - }); + useEffect(() => { + // as it's not pending, after pendingPayment, we know user has (attempted to) submit payment + // (just fire once per flow though) + if (banxaOrder?.status && banxaOrder?.status !== 'pendingPayment') { + if (!hasTrackedExecution.current) { + fireTrackingEvent('funding_payment_executed', { externalId: accountAddress }); + hasTrackedExecution.current = true; + } + } - // clear purchase - setRampPurchase(); - onClose(); + // error: track but let Banxa explain (and user can close dialog) + if (['cancelled', 'declined', 'expired', 'extraVerification'].includes(banxaOrder?.status)) { + fireTrackingEvent('funding_error', { externalId: accountAddress, status: banxaOrder?.status }); + } - // success: clear ramp purchase (don't close funding, let "waiting" handler do that) - } else if (RAMP_PURCHASE_STATUS[rampPurchase?.status]?.isSuccess) { + // complete: we close for them and switch to waiting state + else if (['complete'].includes(banxaOrder?.status)) { // fire success fireTrackingEvent('funding_success', { externalId: accountAddress }); // clear purchase - setRampPurchase(); - setRamping(); // (this should be redundant) - setWaiting(true); - - // processing: switch from ramp widget to "waiting" once PAYMENT_EXECUTED - } else if (rampPurchase?.status === 'PAYMENT_EXECUTED') { - fireTrackingEvent('funding_payment_executed', { externalId: accountAddress }); - setRamping(); + setBanxaOrder(); + setBanxaing(); // (this should be redundant) setWaiting(true); } - }, [rampPurchase?.status]) - + + }, [banxaOrder?.status]); return createPortal( (
- {!waiting && !ramping && !layerswapUrl && ( + {!waiting && !banxaing && !layerswapUrl && ( {fundsNeeded && ( @@ -596,20 +521,6 @@ export const FundingFlow = ({ totalPrice, onClose, onFunded }) => {

Recharge Wallet - - - - RAMP DISCLAIMER: Don't invest unless you're prepared to lose all the money you - invest. This is a high-risk investment and you should not expect to be protected - if something goes wrong.{' '} - Take 2 minutes to learn more. - -

{suggestedAmounts.map((usdc, i) => ( @@ -661,14 +572,15 @@ export const FundingFlow = ({ totalPrice, onClose, onFunded }) => { )}
)} - {ramping && ( + {banxaing && ( <> - -
- -
- -
+ {banxaOrder?.checkoutUrl + ?