Skip to content
Open
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/public/images/gray_square_background.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import BridgeClient from '../../../(with-sidebar)/bridge/BridgeClient';
import { BridgePageProps, initializeBridgePage } from '../../../../utils/bridgePageUtils';

export default async function EmbedPageWrapper({ searchParams, redirectPath }: BridgePageProps) {
await initializeBridgePage(searchParams, redirectPath);
await initializeBridgePage({ searchParams, redirectPath });

return (
<>
Expand Down
19 changes: 19 additions & 0 deletions packages/app/src/app/(embed)/bridge/embed/buy/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Slug } from 'packages/app/src/utils/bridgePageUtils';

import { PathnameEnum } from '@/bridge/constants';

import EmbedPageWrapper from '../../EmbedPageWrapper';

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: once we migrate to turbopack, this can reimport SearchParamsProps type (it need to be awaited)

params: { slug: Slug };
};

export default async function EmbeddedBuyOnrampServicePage({ searchParams, params }: Props) {
return (
<EmbedPageWrapper
searchParams={searchParams}
redirectPath={`${PathnameEnum.EMBED_BUY}/${params.slug}`}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BridgePageProps, initializeBridgePage } from '../../../utils/bridgePage
import BridgeClient from './BridgeClient';

export default async function BridgePageWrapper({ searchParams, redirectPath }: BridgePageProps) {
await initializeBridgePage(searchParams, redirectPath);
await initializeBridgePage({ searchParams, redirectPath });

return (
<main className="bridge-wrapper relative flex h-full flex-1 flex-col overflow-y-auto">
Expand Down
26 changes: 26 additions & 0 deletions packages/app/src/app/(with-sidebar)/bridge/buy/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Metadata } from 'next';
import { Slug } from 'packages/app/src/utils/bridgePageUtils';

import { PathnameEnum } from '@/bridge/constants';

import BridgePageWrapper from '../../BridgePageWrapper';

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

export const metadata: Metadata = {
title: 'On-Ramp to Arbitrum',
description:
"On-ramp directly to Arbitrum with one of several third party providers. Built to scale Ethereum, Arbitrum brings you 10x lower costs while inheriting Ethereum's security model. Arbitrum is a Layer 2 Optimistic Rollup.",
};

export default async function BridgeBuyOnrampServicePage({ searchParams, params }: Props) {
return (
<BridgePageWrapper
searchParams={searchParams}
redirectPath={`${PathnameEnum.BUY}/${params.slug}`}
/>
);
}
10 changes: 5 additions & 5 deletions packages/app/src/utils/bridgePageUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { onrampServices } from '@/bridge/components/BuyPanel/utils';
import { PathnameEnum } from '@/bridge/constants';

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

export type Slug = (typeof onrampServices)[number]['slug'];

export interface BridgePageProps {
searchParams: { [key: string]: string | string[] | undefined };
redirectPath: PathnameEnum;
redirectPath: PathnameEnum | `${PathnameEnum.BUY}/${Slug}` | `${PathnameEnum.EMBED_BUY}/${Slug}`;
}

export async function initializeBridgePage(
searchParams: { [key: string]: string | string[] | undefined },
redirectPath: PathnameEnum,
) {
export async function initializeBridgePage({ searchParams, redirectPath }: BridgePageProps) {
/**
* This code is run on every query param change,
* we don't want to sanitize every query param change.
Expand Down
1 change: 0 additions & 1 deletion packages/app/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ module.exports = {
gradientCelebration: 'linear-gradient(to right, #1B4ADD6F, #E573106F)',
highlight:
'linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.1) 75%, rgba(255, 255, 255, 0))',
eclipse: 'radial-gradient(ellipse 550px 200px at center, #262626 70%, transparent 70%)',
},
colors: {
// ACTION
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed for now because parent are already using use client, but I would explicitely say that it requires it here

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import { ChevronLeftIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';

import { PathnameEnum } from '@/bridge/constants';
import { useMode } from '@/bridge/hooks/useMode';

import { Button } from '../common/Button';

export function BackButton() {
const { embedMode } = useMode();
const searchParams = useSearchParams();

return (
<Link
href={{
pathname: embedMode ? PathnameEnum.EMBED_BUY : PathnameEnum.BUY,
query: searchParams.toString(),
}}
className="flex flex-row justify-content items-center absolute top-4 left-4 gap-2 hover:opacity-80"
>
<Button
variant="secondary"
className="rounded-full w-6 h-6 flex items-center justify-center bg-white/20 backdrop-blur border-none hover:opacity-100 hover:bg-white/20 hover:text-white/70"
>
<ChevronLeftIcon className="h-3 w-3" />
</Button>
<span>Back</span>
</Link>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import { ExclamationCircleIcon } from '@heroicons/react/24/solid';
import { BigNumber, utils } from 'ethers';
import dynamic from 'next/dynamic';
import React, { PropsWithChildren, memo, useCallback } from 'react';
import { usePathname } from 'next/navigation';
import React, { PropsWithChildren, memo, useMemo } from 'react';
import { twMerge } from 'tailwind-merge';
import { Chain } from 'viem';
import { useAccount, useBalance } from 'wagmi';
Expand All @@ -10,69 +12,26 @@ 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 { LinkoutOnrampPanel } from './LinkoutOnrampPanel';
import { MoonPayPanel, MoonPaySkeleton } from './MoonPayPanel';
import { onrampServices } from './utils';

const MoonPayProvider = dynamic(
() => import('@moonpay/moonpay-react').then((mod) => mod.MoonPayProvider),
Expand Down Expand Up @@ -179,7 +138,6 @@ function BuyPanelNetworkButton({
);
}

/* eslint-disable @typescript-eslint/no-unused-vars */
const BalanceWrapper = memo(function BalanceWrapper() {
const { address, isConnected } = useAccount();
const { ethToUSD } = useETHPrice();
Expand All @@ -192,6 +150,13 @@ const BalanceWrapper = memo(function BalanceWrapper() {
error: balanceError,
} = useBalance({ chainId: selectedChainId, address });
const showPriceInUsd = nativeCurrency.symbol.toLowerCase() === 'eth';
const balanceInUsd = useMemo(() => {
if (!balanceState || !showPriceInUsd) {
return null;
}
return ethToUSD(Number(utils.formatEther(BigNumber.from(balanceState.value))));
}, [balanceState, ethToUSD, showPriceInUsd]);
const isBalanceLessThan15Usd = Number(balanceInUsd) < 15;
const [dialogProps, openDialog] = useDialog2();
const openBuyPanelNetworkSelectionDialog = () => {
openDialog('buy_panel_network_selection');
Expand Down Expand Up @@ -228,72 +193,70 @@ const BalanceWrapper = memo(function BalanceWrapper() {
{!isLoadingBalance && (balanceError || typeof balanceState === 'undefined') && (
<span className="text-error">Failed to load balance.</span>
)}
{balanceState && showPriceInUsd && (
<span className="text-white/70">
({formatUSD(ethToUSD(Number(utils.formatEther(BigNumber.from(balanceState.value)))))})
{balanceInUsd !== null && balanceInUsd !== 0 && (
<span className="text-white/50">
{balanceInUsd} {formatUSD(balanceInUsd)}
</span>
)}
</p>

{isBalanceLessThan15Usd && (
<p className="text-sm p-2 rounded-sm bg-white/10 flex gap-2 items-center justify-center leading-none">
<ExclamationCircleIcon className="w-3 h-3" />
<span>Low wallet balance</span>
</p>
)}

<DialogWrapper {...dialogProps} />
</div>
);
});

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-auto pt-4 text-center text-sm', embedMode && 'text-xs')}>
On-Ramps are not endorsed by Arbitrum. Please use at your own risk.
</p>
);
});
}

function OnrampServicePanel() {
const pathname = usePathname();
const onrampService = pathname.split('/').pop();
const allOnrampServices = onrampServices.map((service) => service.slug);

switch (onrampService) {
case 'moonpay':
if (!isMoonPayEnabled) {
return null;
}
return <MoonPayPanel />;
case allOnrampServices.find((service) => service === onrampService):
return <LinkoutOnrampPanel serviceSlug={onrampService!} />;
default:
return <Homepage />;
}
}

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

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

<OnRampProviders>
<MoonPayPanel />
<OnrampServicePanel />
</OnRampProviders>

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