Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ module.exports = {
return [
// Bridge
{
source: '/bridge/:slug((?!^$|api/|_next/|public/|restricted|embed)(?!.*\\.[^/]+$).+)',
source: '/bridge/:slug((?!^$|api/|_next/|public/|restricted|embed|buy)(?!.*\\.[^/]+$).+)',
missing: [
{
type: 'query',
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/banxa.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/coinbase.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/kado.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/mt_pelerin.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/onramp.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/ramp.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/simplex.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/public/images/onramp/transak.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions packages/app/src/app/(embed)/bridge/embed/buy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { addOrbitChainsToArbitrumSDK } from 'packages/app/src/initialization';
import { sanitizeAndRedirect } from 'packages/app/src/utils/sanitizeAndRedirect';

import { Toast } from '@/bridge/components/common/atoms/Toast';

import BridgeClient from '../../../../(with-sidebar)/bridge/BridgeClient';

export default async function EmbededBuyPage({
searchParams,
}: {
searchParams: {
[key: string]: string | string[] | undefined;
};
}) {
if (searchParams.sanitized !== 'true') {
addOrbitChainsToArbitrumSDK();
await sanitizeAndRedirect(searchParams, '/bridge/embed/buy');
}

return (
<>
<BridgeClient />
<Toast />
</>
);
}
91 changes: 91 additions & 0 deletions packages/app/src/app/(with-sidebar)/bridge/buy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Metadata } from 'next';
import { sanitizeAndRedirect } from 'packages/app/src/utils/sanitizeAndRedirect';

import { PORTAL_DOMAIN } from '@/bridge/constants';
import { ChainKeyQueryParam, getChainForChainKeyQueryParam } from '@/bridge/types/ChainQueryParam';
import { isNetwork } from '@/bridge/util/networks';

import { addOrbitChainsToArbitrumSDK } from '../../../../initialization';
import BridgeClient from '../BridgeClient';

type Props = {
searchParams: { [key: string]: string | string[] | undefined };
};

export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
const sourceChainSlug = (
typeof searchParams.sourceChain === 'string' ? searchParams.sourceChain : 'ethereum'
) as ChainKeyQueryParam;
const destinationChainSlug = (
typeof searchParams.destinationChain === 'string'
? searchParams.destinationChain
: 'arbitrum-one'
) as ChainKeyQueryParam;

let sourceChainInfo;
let destinationChainInfo;

try {
sourceChainInfo = getChainForChainKeyQueryParam(sourceChainSlug);
destinationChainInfo = getChainForChainKeyQueryParam(destinationChainSlug);
} catch (error) {
sourceChainInfo = getChainForChainKeyQueryParam('ethereum');
destinationChainInfo = getChainForChainKeyQueryParam('arbitrum-one');
}

const { isOrbitChain: isSourceOrbitChain } = isNetwork(sourceChainInfo.id);
const { isOrbitChain: isDestinationOrbitChain } = isNetwork(destinationChainInfo.id);

const siteTitle = `Bridge to ${destinationChainInfo.name}`;
const siteDescription = `Bridge from ${sourceChainInfo.name} to ${destinationChainInfo.name} using the Arbitrum Bridge. Built to scale Ethereum, Arbitrum brings you 10x lower costs while inheriting Ethereum's security model. Arbitrum is a Layer 2 Optimistic Rollup.`;
const siteDomain = PORTAL_DOMAIN;

let metaImagePath = `${sourceChainInfo.id}-to-${destinationChainInfo.id}.jpg`;

if (isSourceOrbitChain) {
metaImagePath = `${sourceChainInfo.id}.jpg`;
}

if (isDestinationOrbitChain) {
metaImagePath = `${destinationChainInfo.id}.jpg`;
}

const imageUrl = `${siteDomain}/images/__auto-generated/open-graph/${metaImagePath}`;

return {
title: siteTitle,
description: siteDescription,
openGraph: {
url: `${siteDomain}/bridge`,
type: 'website',
title: siteTitle,
description: siteDescription,
images: [imageUrl],
},
twitter: {
card: 'summary_large_image',
site: siteDomain,
title: siteTitle,
description: siteDescription,
images: [imageUrl],
},
};
}

export default async function BridgePage({ searchParams }: Props) {
/**
* This code is run on every query param change,
* we don't want to sanitize every query param change.
* It should only be executed once per user per session.
*/
if (searchParams.sanitized !== 'true') {
addOrbitChainsToArbitrumSDK();
await sanitizeAndRedirect(searchParams, '/bridge/buy');
}

return (
<main className="bridge-wrapper relative flex h-full flex-1 flex-col overflow-y-auto">
<BridgeClient />
</main>
);
}
36 changes: 34 additions & 2 deletions packages/app/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';

import { BUY_EMBED_PATHNAME, BUY_PATHNAME, EMBED_PATHNAME } from '@/bridge/constants';
import { isOnrampEnabled } from '@/bridge/util/featureFlag';

export function middleware(req: NextRequest) {
const url = req.nextUrl;

// Redirect /?mode=embed to /bridge/embed and keep query param (without mode)
// Redirect /?mode=embed to /bridge/embed and keep query params (without mode)
if (url.searchParams.get('mode') === 'embed') {
url.pathname = '/bridge/embed';
url.pathname = EMBED_PATHNAME;
url.searchParams.delete('mode');
return NextResponse.redirect(url, 308);
}

// In embed mode, when buy is disabled
// Redirect /bridge/embed/buy to /bridge/embed and keep query params (without tab)
if (
url.pathname === BUY_EMBED_PATHNAME &&
(!isOnrampEnabled() || url.searchParams.get('disabledFeatures')?.includes('buy'))
) {
url.pathname = '/bridge/embed';
url.searchParams.delete('tab');
return NextResponse.redirect(url, 308);
}

// In normal mode, when buy is disabled
// Redirect /bridge/buy to /bridge and keep query params
if (
url.pathname === BUY_PATHNAME &&
(!isOnrampEnabled() || url.searchParams.get('disabledFeatures')?.includes('buy'))
) {
url.pathname = '/bridge';
url.searchParams.set('tab', 'bridge');
return NextResponse.redirect(url, 308);
}

// Redirect /?tab=buy to /bridge/buy and keep query params (without tab)
if (url.searchParams.get('tab') === 'buy') {
url.pathname = BUY_PATHNAME;
url.searchParams.delete('tab');
return NextResponse.redirect(url, 308);
}

return NextResponse.next();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import { BigNumber, utils } from 'ethers';
import dynamic from 'next/dynamic';
import React, { PropsWithChildren, memo, useCallback } from 'react';
import React, { PropsWithChildren, memo } from 'react';
import { twMerge } from 'tailwind-merge';
import { Chain } from 'viem';
import { useAccount, useBalance } from 'wagmi';
Expand All @@ -10,69 +10,24 @@ import { shallow } from 'zustand/shallow';

import { getProviderForChainId } from '@/token-bridge-sdk/utils';

import { useETHPrice } from '../hooks/useETHPrice';
import { useMode } from '../hooks/useMode';
import { useNativeCurrency } from '../hooks/useNativeCurrency';
import { ChainId } from '../types/ChainId';
import { getAPIBaseUrl } from '../util';
import { formatAmount, formatUSD } from '../util/NumberUtils';
import { isOnrampEnabled, isOnrampServiceEnabled } from '../util/featureFlag';
import { getNetworkName } from '../util/networks';
import { TokenLogoFallback } from './TransferPanel/TokenInfo';
import { Button } from './common/Button';
import { Dialog } from './common/Dialog';
import { DialogProps, DialogWrapper, useDialog2 } from './common/Dialog2';
import { NetworkImage } from './common/NetworkImage';
import { NetworksPanel } from './common/NetworkSelectionContainer';
import { SafeImage } from './common/SafeImage';
import { SearchPanel } from './common/SearchPanel/SearchPanel';
import { Loader } from './common/atoms/Loader';

function MoonPaySkeleton({ children }: PropsWithChildren) {
const { embedMode } = useMode();

return (
<div
className={twMerge(
'relative flex h-full w-full flex-col items-center justify-center overflow-hidden bg-gray-8 p-4 pt-5 text-white md:rounded-lg',
embedMode && 'bg-widget-background',
)}
>
<div className="absolute left-0 top-0 h-[120px] w-full bg-[url('/images/gray_square_background.svg')]"></div>
<div
className={twMerge(
'absolute left-1/2 top-[55px] h-[282px] w-[602px] shrink-0 -translate-x-1/2 bg-eclipse',
embedMode && 'bg-eclipseWidget',
)}
></div>
<div className="relative mb-4 flex flex-col items-center justify-center">
<SafeImage
src="/images/onramp/moonpay.svg"
alt="MoonPay"
width={embedMode ? 45 : 65}
height={embedMode ? 45 : 65}
fallback={<div className="h-8 w-8 min-w-8 rounded-full bg-gray-dark/70" />}
/>
<p className={twMerge('mt-2 text-3xl', embedMode && 'text-xl')}>MoonPay</p>
<p className={twMerge('mt-1 text-xl', embedMode && 'text-sm')}>
PayPal, Debit Card, Apple Pay
</p>
</div>
<div
className={twMerge(
'relative h-full min-h-[600px] w-full',
'[&>div]:!m-0 [&>div]:!w-full [&>div]:!border-x-0 [&>div]:!border-none [&>div]:!p-0 sm:[&>div]:!rounded sm:[&>div]:!border-x',
'[&_iframe]:rounded-xl',
)}
>
{children}
</div>
<p className={twMerge('mt-4 text-center text-sm text-gray-4', embedMode && 'text-xs')}>
On-Ramps are not directly endorsed by Arbitrum. Please use at your own risk.
</p>
</div>
);
}
import { useETHPrice } from '../../hooks/useETHPrice';
import { useMode } from '../../hooks/useMode';
import { useNativeCurrency } from '../../hooks/useNativeCurrency';
import { ChainId } from '../../types/ChainId';
import { formatAmount, formatUSD } from '../../util/NumberUtils';
import { isOnrampEnabled, isOnrampServiceEnabled } from '../../util/featureFlag';
import { getNetworkName } from '../../util/networks';
import { TokenLogoFallback } from '../TransferPanel/TokenInfo';
import { Button } from '../common/Button';
import { Dialog } from '../common/Dialog';
import { DialogProps, DialogWrapper, useDialog2 } from '../common/Dialog2';
import { NetworkImage } from '../common/NetworkImage';
import { NetworksPanel } from '../common/NetworkSelectionContainer';
import { SafeImage } from '../common/SafeImage';
import { SearchPanel } from '../common/SearchPanel/SearchPanel';
import { Loader } from '../common/atoms/Loader';
import { Homepage } from './Homepage';
import { MoonPaySkeleton } from './MoonPayPanel';

const MoonPayProvider = dynamic(
() => import('@moonpay/moonpay-react').then((mod) => mod.MoonPayProvider),
Expand All @@ -84,6 +39,7 @@ const MoonPayProvider = dynamic(

const isMoonPayEnabled = isOnrampServiceEnabled('moonpay');

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function OnRampProviders({ children }: PropsWithChildren) {
if (!isOnrampEnabled()) {
return children;
Expand Down Expand Up @@ -169,7 +125,7 @@ function BuyPanelNetworkButton({
const selectedChainId = useBuyPanelStore((state) => state.selectedChainId);

return (
<Button variant="secondary" onClick={onClick} className="border-white/30">
<Button variant="secondary" onClick={onClick} className="border-[#333333]">
<div className="flex flex-nowrap items-center gap-1 text-lg leading-[1.1]">
<NetworkImage chainId={selectedChainId} className="h-4 w-4 p-[2px]" size={20} />
{getNetworkName(selectedChainId)}
Expand All @@ -179,7 +135,6 @@ function BuyPanelNetworkButton({
);
}

/* eslint-disable @typescript-eslint/no-unused-vars */
const BalanceWrapper = memo(function BalanceWrapper() {
const { address, isConnected } = useAccount();
const { ethToUSD } = useETHPrice();
Expand Down Expand Up @@ -229,7 +184,7 @@ const BalanceWrapper = memo(function BalanceWrapper() {
<span className="text-error">Failed to load balance.</span>
)}
{balanceState && showPriceInUsd && (
<span className="text-white/70">
<span className="text-white/50">
({formatUSD(ethToUSD(Number(utils.formatEther(BigNumber.from(balanceState.value)))))})
</span>
)}
Expand All @@ -240,60 +195,40 @@ const BalanceWrapper = memo(function BalanceWrapper() {
);
});

const MoonPayPanel = memo(function MoonPayPanel() {
const { address } = useAccount();
const showMoonPay = isOnrampServiceEnabled('moonpay');

const handleGetSignature = useCallback(async (widgetUrl: string): Promise<string> => {
const response = await fetch(`${getAPIBaseUrl()}/api/moonpay`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: widgetUrl }),
});
const { signature } = await response.json();
return signature;
}, []);

if (!showMoonPay) {
return null;
}

const MoonPayBuyWidget = dynamic(
() => import('@moonpay/moonpay-react').then((mod) => mod.MoonPayBuyWidget),
{
ssr: false,
},
);
function OnrampDisclaimer() {
const { embedMode } = useMode()

return (
<MoonPaySkeleton>
<MoonPayBuyWidget
variant="embedded"
walletAddress={address}
baseCurrencyCode="usd"
defaultCurrencyCode="eth"
onUrlSignatureRequested={handleGetSignature}
visible
/>
</MoonPaySkeleton>
);
});
<p
className={twMerge(
'text-gray-4 mt-4 text-center text-sm',
embedMode && 'text-xs'
)}
>
On-Ramps are not endorsed by Arbitrum. Please use at your own risk.
</p>
)
}

export function BuyPanel() {
const { embedMode } = useMode();

return (
<div
className={twMerge(
'overflow-hidden rounded-lg pb-8 text-white',
'bg-gray-1 rounded-md border border-white/30 px-6 py-7 pb-8 text-white w-full sm:max-w-[600px]',
embedMode && 'mx-auto max-w-[540px]',
)}
>
<OnRampProviders>
<BalanceWrapper />

<Homepage />

{/* <OnRampProviders>
<MoonPayPanel />
</OnRampProviders>
</OnRampProviders> */}

<OnrampDisclaimer />
</div>
);
}
Loading
Loading