mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
New bridge app + explorer
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"no-conditional-literals-in-jsx"
|
||||
],
|
||||
"rules": {
|
||||
"no-conditional-literals-in-jsx/no-conditional-literals-in-jsx": "error",
|
||||
"no-conditional-literals-in-jsx/no-unwrapped-jsx-text": "error",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react/display-name": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# .github/workflows/chromatic.yml
|
||||
|
||||
# Workflow name
|
||||
name: 'Chromatic'
|
||||
|
||||
# Event for the workflow
|
||||
on: push
|
||||
|
||||
# List of jobs
|
||||
jobs:
|
||||
chromatic-deployment:
|
||||
# Operating System
|
||||
runs-on: ubuntu-latest
|
||||
# Job steps
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Install dependencies
|
||||
# 👇 Install dependencies with the same package manager used in the project (replace it as needed), e.g. yarn, npm, pnpm
|
||||
run: yarn
|
||||
# 👇 Adds Chromatic as a step in the workflow
|
||||
- name: Publish to Chromatic
|
||||
uses: chromaui/action@v1
|
||||
# Chromatic GitHub Action options
|
||||
with:
|
||||
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Dev to dev-prod && dev-stage
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
rebase-dev-prod:
|
||||
timeout-minutes: 2
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set Git config
|
||||
run: |
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "Github Actions"
|
||||
- name: Merge dev to dev-prod
|
||||
run: |
|
||||
git fetch --unshallow
|
||||
git checkout dev-prod
|
||||
git rebase dev
|
||||
git push
|
||||
|
||||
rebase-dev-stage:
|
||||
timeout-minutes: 2
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set Git config
|
||||
run: |
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "Github Actions"
|
||||
- name: Merge dev to dev-stage
|
||||
run: |
|
||||
git fetch --unshallow
|
||||
git checkout dev-stage
|
||||
git rebase dev
|
||||
git push
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Main to main-sandbox
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
rebase-main-sandbox:
|
||||
timeout-minutes: 2
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set Git config
|
||||
run: |
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "Github Actions"
|
||||
- name: Merge main to main-sandbox
|
||||
run: |
|
||||
git fetch --unshallow
|
||||
git checkout main-sandbox
|
||||
git rebase main
|
||||
git push
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
*node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.vercel
|
||||
|
||||
# Moving to yarn
|
||||
package-lock.json
|
||||
storybook-static/*
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { StorybookConfig } from "@storybook/nextjs";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../stories/**/*.mdx",
|
||||
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-mdx-gfm",
|
||||
"storybook-addon-mock"
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/nextjs",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
config.module?.rules?.push({
|
||||
test: /\.scss$/,
|
||||
use: ["style-loader", "css-loader", "postcss-loader", "sass-loader"],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { StorybookConfig } from "@storybook/nextjs";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../stories/**/*.mdx",
|
||||
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-mdx-gfm",
|
||||
"storybook-addon-mock"
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/nextjs",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
config.module?.rules?.push({
|
||||
test: /\.scss$/,
|
||||
use: ["style-loader", "css-loader", "postcss-loader", "sass-loader"],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../styles/globals.css";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
loaders: [
|
||||
async () => ({
|
||||
settings: await (await fetch(`https://bridge-api.bridge.lux.network/api/settings?version=sandbox`)).json(),
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../styles/globals.css";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
loaders: [
|
||||
async () => ({
|
||||
settings: await (await fetch(`https://bridge-api.bridge.lux.network/api/settings?version=sandbox`)).json(),
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "errors",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"revealProblems": "never"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "errors",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"revealProblems": "never"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "errors",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"revealProblems": "never"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run debug",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "errors",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"revealProblems": "never"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# bridge-next
|
||||
Next generation LUX Bridge.
|
||||
|
||||
## To run locally
|
||||
|
||||
Install `pnpm` [like so](https://pnpm.io/installation)
|
||||
|
||||
The usual scripts for a Next site, using `pnpm`
|
||||
```
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Since "pnpm" is a finger twister, many people alias it to "pn". For example, with `bash`, put `alias pn='pnpm'` in `.bashrc`.
|
||||
@@ -0,0 +1,18 @@
|
||||
export type ApiError = {
|
||||
code: LSAPIKnownErrorCode | string,
|
||||
message: string;
|
||||
}
|
||||
|
||||
export enum LSAPIKnownErrorCode {
|
||||
NOT_FOUND = "NOT_FOUND",
|
||||
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
|
||||
INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
|
||||
FUNDS_ON_HOLD = "FUNDS_ON_HOLD_ERROR",
|
||||
COINBASE_AUTHORIZATION_LIMIT_EXCEEDED = "COINBASE_AUTHORIZATION_LIMIT_EXCEEDED",
|
||||
COINBASE_INVALID_2FA = "COINBASE_INVALID_2FA",
|
||||
ACTIVE_SWAP_LIMIT_EXCEEDED = "ACTIVE_SWAP_LIMIT_EXCEEDED",
|
||||
NETWORK_ACCOUNT_ALREADY_EXISTS = "NETWORK_ACCOUNT_ALREADY_EXISTS",
|
||||
BLACKLISTED_ADDRESS = "BLACKLISTED_ADDRESS",
|
||||
INVALID_ADDRESS_ERROR = "INVALID_ADDRESS_ERROR",
|
||||
UNACTIVATED_ADDRESS_ERROR = "UNACTIVATED_ADDRESS_ERROR"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiError } from "./ApiError";
|
||||
|
||||
export class EmptyApiResponse {
|
||||
constructor(error?: ApiError) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
error?: ApiError;
|
||||
}
|
||||
|
||||
export class ApiResponse<T> extends EmptyApiResponse {
|
||||
data?: T
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Wallet } from "../stores/walletStore"
|
||||
import { Currency } from "./Currency"
|
||||
import { Layer } from "./Layer"
|
||||
|
||||
export type BalanceProps = {
|
||||
layer: Layer,
|
||||
address: string
|
||||
}
|
||||
|
||||
export type GasProps = {
|
||||
layer: Layer,
|
||||
currency: Currency,
|
||||
address?: `0x${string}`,
|
||||
userDestinationAddress?: string,
|
||||
wallet?: Wallet
|
||||
}
|
||||
|
||||
export type Balance = {
|
||||
network: string,
|
||||
amount: number,
|
||||
decimals: number,
|
||||
isNativeCurrency: boolean,
|
||||
token: string,
|
||||
request_time: string,
|
||||
}
|
||||
|
||||
export type Gas = {
|
||||
token: string,
|
||||
gas: number,
|
||||
gasDetails?: {
|
||||
gasLimit?: number,
|
||||
maxFeePerGas?: number,
|
||||
gasPrice?: number,
|
||||
maxPriorityFeePerGas?: number
|
||||
},
|
||||
request_time: string
|
||||
}
|
||||
|
||||
export type BalanceProvider = {
|
||||
getBalance: ({ layer, address }: BalanceProps) => Promise<Balance[] | undefined> | undefined | void,
|
||||
getGas: ({ layer, address, currency, userDestinationAddress, wallet }: GasProps) => Promise<Gas[] | undefined> | undefined | void,
|
||||
supportedNetworks: string[],
|
||||
name: string,
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { LayerStatus } from "./Layer";
|
||||
|
||||
export enum NetworkType {
|
||||
EVM = "evm",
|
||||
Starknet = "starknet",
|
||||
Solana = "solana",
|
||||
Cosmos = "cosmos",
|
||||
StarkEx = "stark_ex",
|
||||
ZkSyncLite = "zk_sync_lite",
|
||||
TON = 'ton'
|
||||
}
|
||||
|
||||
|
||||
export class CryptoNetwork {
|
||||
display_name: string;
|
||||
internal_name: string;
|
||||
native_currency: string | null | undefined;
|
||||
average_completion_time: string;
|
||||
transaction_explorer_template: string;
|
||||
account_explorer_template?: string;
|
||||
status: LayerStatus;
|
||||
currencies: NetworkCurrency[];
|
||||
refuel_amount_in_usd: number;
|
||||
chain_id: string;
|
||||
type: NetworkType;
|
||||
created_date: string;
|
||||
is_featured: boolean;
|
||||
nodes: NetworkNode[];
|
||||
managed_accounts: ManagedAccount[];
|
||||
metadata: Metadata | null | undefined;
|
||||
img_url?: string
|
||||
}
|
||||
|
||||
export class NetworkCurrency {
|
||||
name: string;
|
||||
asset: string;
|
||||
status: LayerStatus;
|
||||
is_deposit_enabled: boolean;
|
||||
is_withdrawal_enabled: boolean;
|
||||
is_refuel_enabled: boolean;
|
||||
max_withdrawal_amount: number;
|
||||
deposit_fee: number;
|
||||
withdrawal_fee: number;
|
||||
//TODO may be plain string
|
||||
contract_address: `0x${string}` | null | undefined;
|
||||
decimals: number;
|
||||
source_base_fee: number;
|
||||
destination_base_fee: number;
|
||||
}
|
||||
export class NetworkNode {
|
||||
url: string;
|
||||
}
|
||||
export class ManagedAccount {
|
||||
address: `0x${string}`;
|
||||
}
|
||||
export class Metadata {
|
||||
multicall3?: {
|
||||
address: `0x${string}`
|
||||
blockCreated: number
|
||||
}
|
||||
ensRegistry?: {
|
||||
address: `0x${string}`
|
||||
}
|
||||
ensUniversalResolver?: {
|
||||
address: `0x${string}`
|
||||
}
|
||||
WatchdogContractAddress?: `0x${string}`
|
||||
L1Network?: string
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class Currency {
|
||||
asset: string;
|
||||
usd_price: number;
|
||||
precision: number;
|
||||
img_url?: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class CurrencyExchange {
|
||||
exchange_id: string;
|
||||
fee: number;
|
||||
is_off_ramp_enabled: boolean;
|
||||
min_withdrawal_amount: number;
|
||||
off_ramp_max_amount:number;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NetworkCurrency } from "./CryptoNetwork";
|
||||
import { LayerStatus } from "./Layer";
|
||||
|
||||
export class Exchange {
|
||||
display_name: string;
|
||||
internal_name: string;
|
||||
authorization_flow: "o_auth2" | "api_credentials" | 'none'
|
||||
currencies: (ExchangeCurrency & NetworkCurrency)[];
|
||||
status: LayerStatus;
|
||||
type: "cex" | "fiat";
|
||||
created_date: string;
|
||||
is_featured: boolean;
|
||||
img_url?: string
|
||||
}
|
||||
|
||||
export class ExchangeCurrency {
|
||||
asset: string;
|
||||
withdrawal_fee: number;
|
||||
min_deposit_amount: number;
|
||||
network: string;
|
||||
is_default: boolean;
|
||||
status: LayerStatus
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { CryptoNetwork, Metadata, NetworkType } from "./CryptoNetwork";
|
||||
|
||||
export type LayerStatus = "active" | "inactive" | 'insufficient_liquidity';
|
||||
export type Layer = {
|
||||
display_name: string;
|
||||
internal_name: string;
|
||||
status: LayerStatus;
|
||||
is_featured: boolean;
|
||||
created_date: string;
|
||||
} & LayerData
|
||||
|
||||
type LayerData = ({
|
||||
isExchange: true;
|
||||
assets?: ExchangeAsset[];
|
||||
type: "cex" | "fiat",
|
||||
authorization_flow: "o_auth2" | "api_credentials" | 'none';
|
||||
} | {
|
||||
isExchange: false;
|
||||
assets: NetworkAsset[];
|
||||
native_currency: string | null | undefined;
|
||||
average_completion_time: string;
|
||||
chain_id: string | null | undefined;
|
||||
type: NetworkType,
|
||||
metadata: Metadata | null | undefined;
|
||||
nodes: NetworkNodes[];
|
||||
})
|
||||
|
||||
export type BaseL2Asset = {
|
||||
asset: string;
|
||||
network_internal_name: string;
|
||||
network?: CryptoNetwork;
|
||||
is_default: boolean;
|
||||
status: LayerStatus;
|
||||
}
|
||||
|
||||
export type ExchangeAsset = {
|
||||
withdrawal_fee: number;
|
||||
min_deposit_amount: number;
|
||||
} & BaseL2Asset
|
||||
|
||||
export type NetworkAsset = {
|
||||
contract_address?: `0x${string}` | null | undefined
|
||||
decimals: number
|
||||
} & BaseL2Asset
|
||||
|
||||
export type NetworkNodes = {
|
||||
url: string;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { CryptoNetwork, NetworkCurrency } from "./CryptoNetwork";
|
||||
import { Currency } from "./Currency";
|
||||
import { Exchange, ExchangeCurrency } from "./Exchange";
|
||||
import { BaseL2Asset, ExchangeAsset, Layer, NetworkAsset } from "./Layer";
|
||||
import { BridgeSettings } from "./BridgeSettings";
|
||||
import { Partner } from "./Partner";
|
||||
|
||||
export class BridgeAppSettings extends BridgeSettings {
|
||||
constructor(settings: BridgeSettings | any) {
|
||||
super();
|
||||
Object.assign(this, BridgeAppSettings.ResolveSettings(settings))
|
||||
|
||||
this.layers = BridgeAppSettings.ResolveLayers(this.exchanges, this.networks);
|
||||
}
|
||||
|
||||
layers: Layer[]
|
||||
|
||||
resolveImgSrc = (item: Layer | Currency | Pick<Layer, 'internal_name'> | { asset: string } | Partner) => {
|
||||
|
||||
if (!item) {
|
||||
return "/images/logo_placeholder.png";
|
||||
}
|
||||
|
||||
const basePath = new URL(this.discovery.resource_storage_url);
|
||||
|
||||
// Shitty way to check for partner
|
||||
if ((item as Partner).is_wallet != undefined) {
|
||||
return (item as Partner)?.logo_url;
|
||||
}
|
||||
else if ((item as any)?.internal_name != undefined) {
|
||||
basePath.pathname = `/bridge/networks/${(item as any)?.internal_name?.toLowerCase()}.png`;
|
||||
}
|
||||
else if ((item as any)?.asset != undefined) {
|
||||
basePath.pathname = `/bridge/currencies/${(item as any)?.asset?.toLowerCase()}.png`;
|
||||
}
|
||||
|
||||
return basePath.href;
|
||||
}
|
||||
|
||||
static ResolveSettings(settings: BridgeSettings) {
|
||||
const basePath = new URL(settings.discovery.resource_storage_url);
|
||||
|
||||
settings.networks = settings.networks.map(n => ({
|
||||
...n,
|
||||
img_url: `${basePath}bridge/networks/${n?.internal_name?.toLowerCase()}.png`
|
||||
}))
|
||||
settings.exchanges = settings.exchanges.map(e => ({
|
||||
...e,
|
||||
img_url: `${basePath}bridge/networks/${e?.internal_name?.toLowerCase()}.png`
|
||||
}))
|
||||
settings.currencies = settings.currencies.map(c => ({
|
||||
...c,
|
||||
img_url: `${basePath}bridge/networks/${c?.asset?.toLowerCase()}.png`
|
||||
}))
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
static ResolveLayers(exchanges: Exchange[], networks: CryptoNetwork[]): Layer[] {
|
||||
const exchangeLayers: Layer[] = exchanges.map((e): Layer => ({
|
||||
isExchange: true,
|
||||
assets: BridgeAppSettings.ResolveExchangeL2Assets(e.currencies, networks),
|
||||
...e
|
||||
}))
|
||||
const networkLayers: Layer[] = networks.map((n): Layer =>
|
||||
({
|
||||
isExchange: false,
|
||||
assets: BridgeAppSettings.ResolveNetworkL2Assets(n),
|
||||
...n
|
||||
}))
|
||||
const result = exchangeLayers.concat(networkLayers)
|
||||
return result
|
||||
}
|
||||
|
||||
static ResolveExchangeL2Assets(
|
||||
currencies: ExchangeCurrency[],
|
||||
networks: CryptoNetwork[]): ExchangeAsset[] {
|
||||
return currencies.map(exchangecurrency => {
|
||||
const network = networks.find(n => n.internal_name === exchangecurrency.network) as CryptoNetwork
|
||||
const networkCurrencies = network?.currencies.find(nc => nc.asset === exchangecurrency.asset) as NetworkCurrency
|
||||
const res: ExchangeAsset = {
|
||||
asset: exchangecurrency.asset,
|
||||
status: exchangecurrency.status,
|
||||
is_default: exchangecurrency.is_default,
|
||||
network_internal_name: exchangecurrency.network,
|
||||
network: { ...network, currencies: [networkCurrencies] },
|
||||
min_deposit_amount: exchangecurrency.min_deposit_amount,
|
||||
withdrawal_fee: exchangecurrency.withdrawal_fee,
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
static ResolveNetworkL2Assets(network: CryptoNetwork): NetworkAsset[] {
|
||||
return network?.currencies.map(c => ({
|
||||
asset: c.asset,
|
||||
status: c.status,
|
||||
is_default: true,
|
||||
network_internal_name: network?.internal_name,
|
||||
network: { ...network },
|
||||
contract_address: c.contract_address,
|
||||
decimals: c.decimals
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export class AuthGetCodeResponse {
|
||||
data: {
|
||||
next: Date,
|
||||
already_sent: boolean
|
||||
};
|
||||
error: string;
|
||||
}
|
||||
|
||||
export class AuthConnectResponse {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
token_type: string;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { CryptoNetwork } from "./CryptoNetwork";
|
||||
import { Currency } from "./Currency";
|
||||
import { Exchange } from "./Exchange";
|
||||
|
||||
export class BridgeSettings {
|
||||
exchanges: Exchange[];
|
||||
networks: CryptoNetwork[];
|
||||
currencies: Currency[];
|
||||
discovery: {
|
||||
identity_url: string;
|
||||
resource_storage_url: string;
|
||||
o_auth_providers: OauthProveider[]
|
||||
}
|
||||
validSignatureisPresent?: boolean;
|
||||
};
|
||||
|
||||
type OauthProveider = {
|
||||
provider: string,
|
||||
oauth_connect_url: string,
|
||||
oauth_authorize_url: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class Partner {
|
||||
display_name: string;
|
||||
logo_url: string;
|
||||
is_wallet: boolean;
|
||||
id: number;
|
||||
name: string
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
export class PersistantQueryParams {
|
||||
from?: string = "";
|
||||
to?: string = "";
|
||||
lockAddress?: boolean = false;
|
||||
lockFrom?: boolean = false;
|
||||
lockTo?: boolean = false;
|
||||
lockAsset?: boolean = false;
|
||||
destAddress?: string = "";
|
||||
hideRefuel?: boolean = false;
|
||||
hideAddress?: boolean = false;
|
||||
hideFrom?: boolean = false;
|
||||
hideTo?: boolean = false;
|
||||
asset?: string = "";
|
||||
amount?: string = "";
|
||||
externalId?: string = ""
|
||||
signature?: string = "";
|
||||
timestamp?: string = "";
|
||||
apiKey?: string = "";
|
||||
balances?: string = "";
|
||||
account?: string = "";
|
||||
actionButtonText?: string = "";
|
||||
theme?: string = "";
|
||||
appName?: string = "";
|
||||
|
||||
// Obsolate
|
||||
sourceExchangeName?: string = "";
|
||||
destNetwork?: string = "";
|
||||
lockNetwork?: boolean = false;
|
||||
lockExchange?: boolean = false;
|
||||
addressSource?: string = "";
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class QueryParams extends PersistantQueryParams {
|
||||
coinbase_redirect?: string = "";
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum SwapFailReasons {
|
||||
RECEIVED_LESS_THAN_VALID_RANGE = "received_less_than_valid_range",
|
||||
RECEIVED_MORE_THAN_VALID_RANGE = "received_more_than_valid_range"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export enum SwapStatus {
|
||||
Created = 'created',
|
||||
|
||||
UserTransferPending= 'user_transfer_pending',
|
||||
UserTransferDelayed = 'user_transfer_delayed',
|
||||
LsTransferPending = "ls_transfer_pending",
|
||||
|
||||
Completed = 'completed',
|
||||
Failed = 'failed',
|
||||
Expired = "expired",
|
||||
Cancelled = "cancelled",
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
|
||||
export type ThemeData = {
|
||||
backdrop?: string,
|
||||
actionButtonText: string,
|
||||
logo: string,
|
||||
placeholderText: string,
|
||||
primary: ThemeColor,
|
||||
secondary?: ThemeColor
|
||||
}
|
||||
|
||||
export type ThemeColor = {
|
||||
DEFAULT: string;
|
||||
50: string;
|
||||
100: string;
|
||||
200: string;
|
||||
300: string;
|
||||
400: string;
|
||||
500: string;
|
||||
600: string;
|
||||
700: string;
|
||||
800: string;
|
||||
900: string;
|
||||
950?: string;
|
||||
text: string,
|
||||
textMuted?: string,
|
||||
}
|
||||
|
||||
export const THEME_COLORS: { [key: string]: ThemeData } = {
|
||||
"imxMarketplace": {
|
||||
backdrop: "0, 121, 133",
|
||||
actionButtonText: '0, 0, 0',
|
||||
placeholderText: '140, 152, 192',
|
||||
logo: '255, 255, 255',
|
||||
primary: {
|
||||
DEFAULT: '46, 236, 255',
|
||||
'50': '230, 253, 255',
|
||||
'100': '209, 251, 255',
|
||||
'200': '168, 247, 255',
|
||||
'300': '128, 243, 255',
|
||||
'400': '87, 240, 255',
|
||||
'500': '46, 236, 255',
|
||||
'600': '0, 232, 255',
|
||||
'700': '0, 172, 189',
|
||||
'800': '0, 121, 133',
|
||||
'900': '0, 70, 77',
|
||||
'text': '255, 255, 255',
|
||||
'textMuted': '86, 97, 123',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '17, 29, 54',
|
||||
'50': '49, 60, 155',
|
||||
'100': '46, 59, 147',
|
||||
'200': '35, 42, 112',
|
||||
'300': '32, 41, 101',
|
||||
'400': '28, 39, 89',
|
||||
'500': '22, 37, 70',
|
||||
'600': '20, 33, 62',
|
||||
'700': '17, 29, 54',
|
||||
'800': '15, 25, 47',
|
||||
'900': '12, 21, 39',
|
||||
'950': '11, 17, 35',
|
||||
'text': '209, 251, 255',
|
||||
},
|
||||
},
|
||||
"ea7df14a1597407f9f755f05e25bab42": {
|
||||
backdrop: "0, 121, 133",
|
||||
placeholderText: '198, 242, 246',
|
||||
actionButtonText: '0, 0, 0',
|
||||
logo: '255, 255, 255',
|
||||
primary: {
|
||||
DEFAULT: '128, 226, 235',
|
||||
'50': '255, 255, 255',
|
||||
'100': '255, 255, 255',
|
||||
'200': '234, 250, 252',
|
||||
'300': '198, 242, 246',
|
||||
'400': '163, 234, 241',
|
||||
'500': '128, 226, 235',
|
||||
'600': '80, 215, 227',
|
||||
'700': '34, 201, 217',
|
||||
'800': '26, 156, 168',
|
||||
'900': '19, 111, 120',
|
||||
'950': '15, 89, 96',
|
||||
'text': '255, 255, 255',
|
||||
'textMuted': '86, 97, 123',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '46, 89, 112',
|
||||
'50': '193, 217, 230',
|
||||
'100': '179, 208, 224',
|
||||
'200': '150, 191, 212',
|
||||
'300': '121, 173, 200',
|
||||
'400': '92, 155, 188',
|
||||
'500': '34, 66, 83',
|
||||
'600': '16, 35, 49',
|
||||
'700': '15, 29, 39',
|
||||
'800': '34, 66, 83',
|
||||
'900': '22, 43, 54',
|
||||
'950': '14, 27, 34',
|
||||
'text': '209, 251, 255',
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
placeholderText: '134, 134, 134',
|
||||
actionButtonText: '255, 255, 255',
|
||||
logo: '255, 0, 147',
|
||||
primary: {
|
||||
DEFAULT: '228, 37, 117',
|
||||
'50': '248, 200, 220',
|
||||
'100': '246, 182, 209',
|
||||
'200': '241, 146, 186',
|
||||
'300': '237, 110, 163',
|
||||
'400': '232, 73, 140',
|
||||
'500': '228, 37, 117',
|
||||
'600': '166, 51, 94',
|
||||
'700': '136, 17, 67',
|
||||
'800': '147, 8, 99',
|
||||
'900': '196, 153, 175',
|
||||
'text': '17, 17, 17',
|
||||
'textMuted': '86, 97, 123',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '240, 240, 240',
|
||||
'50': '49, 60, 155',
|
||||
'100': '46, 59, 147',
|
||||
'200': '134, 134, 134',
|
||||
'300': '139, 139, 139',
|
||||
'400': '177, 177, 177',
|
||||
'500': '218, 218, 218',
|
||||
'600': '223, 223, 223',
|
||||
'700': '240, 240, 240',
|
||||
'800': '243, 244, 246',
|
||||
'900': '250, 248, 248',
|
||||
'950': '255, 255, 255',
|
||||
'text': '108, 108, 108',
|
||||
},
|
||||
},
|
||||
"default": {
|
||||
backdrop: "62, 18, 64",
|
||||
placeholderText: '140, 152, 192',
|
||||
actionButtonText: '255, 255, 255',
|
||||
logo: '255, 0, 147',
|
||||
primary: {
|
||||
DEFAULT: '228, 37, 117',
|
||||
'50': '248, 200, 220',
|
||||
'100': '246, 182, 209',
|
||||
'200': '241, 146, 186',
|
||||
'300': '237, 110, 163',
|
||||
'400': '232, 73, 140',
|
||||
'500': '228, 37, 117',
|
||||
'600': '166, 51, 94',
|
||||
'700': '136, 17, 67',
|
||||
'800': '147, 8, 99',
|
||||
'900': '110, 0, 64',
|
||||
'text': '255, 255, 255',
|
||||
'textMuted': '86, 97, 123',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '17, 29, 54',
|
||||
'50': '49, 60, 155',
|
||||
'100': '46, 59, 147',
|
||||
'200': '35, 42, 112',
|
||||
'300': '32, 41, 101',
|
||||
'400': '28, 39, 89',
|
||||
'500': '22, 37, 70',
|
||||
'600': '20, 33, 62',
|
||||
'700': '17, 29, 54',
|
||||
'800': '15, 25, 47',
|
||||
'900': '12, 21, 39',
|
||||
'950': '11, 17, 35',
|
||||
'text': '171, 181, 209',
|
||||
},
|
||||
},
|
||||
"evmos": {
|
||||
placeholderText: '128, 110, 107',
|
||||
actionButtonText: '255, 255, 255',
|
||||
logo: '226, 49, 115',
|
||||
primary: {
|
||||
DEFAULT: '237, 78, 51',
|
||||
'50': '248, 200, 220',
|
||||
'100': '246, 182, 209',
|
||||
'200': '241, 146, 186',
|
||||
'300': '237, 110, 163',
|
||||
'400': '232, 73, 140',
|
||||
'500': '219, 211, 209',
|
||||
'600': '166, 51, 94',
|
||||
'700': '136, 17, 67',
|
||||
'800': '147, 8, 99',
|
||||
'900': '237, 78, 51',
|
||||
'text': '74, 61, 59',
|
||||
'textMuted': '86, 97, 123',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '239, 239, 239',
|
||||
'50': '49, 60, 155',
|
||||
'100': '46, 59, 147',
|
||||
'200': '134, 134, 134',
|
||||
'300': '139, 139, 139',
|
||||
'400': '177, 177, 177',
|
||||
'500': '227, 227, 227',
|
||||
'600': '223, 223, 223',
|
||||
'700': '244, 243, 242',
|
||||
'800': '248, 247, 247',
|
||||
'900': '250, 248, 248',
|
||||
'950': '255, 255, 255',
|
||||
'text': '122, 91, 91',
|
||||
},
|
||||
},
|
||||
"ton": {
|
||||
placeholderText: '134, 134, 134',
|
||||
actionButtonText: '255, 255, 255',
|
||||
logo: '15, 15, 15',
|
||||
primary: {
|
||||
DEFAULT: '51, 144, 236',
|
||||
'50': '248, 200, 220',
|
||||
'100': '246, 182, 209',
|
||||
'200': '241, 146, 186',
|
||||
'300': '237, 110, 163',
|
||||
'400': '232, 73, 140',
|
||||
'500': '45, 148, 229',
|
||||
'600': '166, 51, 94',
|
||||
'700': '136, 17, 67',
|
||||
'800': '45, 148, 229',
|
||||
'900': '51, 144, 236',
|
||||
'text': '15, 15, 15',
|
||||
'textMuted': '86, 97, 123',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '240, 240, 240',
|
||||
'50': '190, 195, 200',
|
||||
'100': '199, 201, 206',
|
||||
'200': '208, 210, 211',
|
||||
'300': '212, 214, 219',
|
||||
'400': '220, 222, 226',
|
||||
'500': '227, 230, 233',
|
||||
'600': '229, 231, 235',
|
||||
'700': '241, 243, 245',
|
||||
'800': '243, 244, 246',
|
||||
'900': '250, 248, 248',
|
||||
'950': '255, 255, 255',
|
||||
'text': '106, 119, 133',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { FC } from "react"
|
||||
|
||||
export type FormSteps = "SwapForm" | "Email" | "Code" | "OffRampExchangeOAuth" | "ExchangeOAuth" | "ExchangeApiCredentials" | "SwapConfirmation"
|
||||
|
||||
export type SwapSteps = "Email" | "Code" | "Overview" | "Withdrawal" | "OffRampWithdrawal" | "Processing" | "Success" | "Failed" | "ExternalPayment"
|
||||
export type LoginSteps = "Email" | "Code"
|
||||
|
||||
export type BaseWizard = {
|
||||
[key: string]: SwapCreateStep
|
||||
}
|
||||
|
||||
export type FormWizardSteps = {
|
||||
[Property in FormSteps]: SwapCreateStep
|
||||
}
|
||||
export type SwapWizardSteps = {
|
||||
[Property in SwapSteps]: SwapCreateStep
|
||||
}
|
||||
export type LoginWizardSteps = {
|
||||
[Property in LoginSteps]: SwapCreateStep
|
||||
}
|
||||
|
||||
export enum SwapCreateStep {
|
||||
MainForm = "MainForm",
|
||||
Email = "Email",
|
||||
Code = "Code",
|
||||
PendingSwaps = "PendingSwaps",
|
||||
AuthorizeCoinbaseWithdrawal = "AuthorizeCoinbaseWithdrawal",
|
||||
OffRampOAuth = "OffRampOAuth",
|
||||
ApiKey = "ApiKey",
|
||||
TwoFactor = "TwoFactor",
|
||||
ActiveSwapLimit = 'ActiveSwapLimit',
|
||||
Error = "Error"
|
||||
}
|
||||
|
||||
export enum SwapWithdrawalStep {
|
||||
Withdrawal = "Withdrawal",
|
||||
CoinbaseManualWithdrawal = "CoinbaseManualWithdrawal",
|
||||
SwapProcessing = 'SwapProcessing',
|
||||
ProcessingWalletTransaction = "ProcessingWalletTransaction",
|
||||
Success = "Success",
|
||||
Failed = "Failed",
|
||||
Error = "Error",
|
||||
Delay = "Delay",
|
||||
OffRampWithdrawal = "OffRampWithdrawal",
|
||||
WithdrawFromImtblx = "WithdrawFromImtblx",
|
||||
WithdrawFromStarknet = "WithdrawFromStarknet",
|
||||
SelectWithdrawalType = "SelectWithdrawalType",
|
||||
CoinbaseInternalWithdrawal = "CoinbaseInternalWithdrawal",
|
||||
}
|
||||
|
||||
export enum AuthStep {
|
||||
Email = "Email",
|
||||
Code = "Code",
|
||||
PendingSwaps = 'PendingSwaps'
|
||||
}
|
||||
|
||||
export type Steps = AuthStep | SwapWithdrawalStep | SwapCreateStep
|
||||
|
||||
export const ExchangeAuthorizationSteps: { [key: string]: SwapCreateStep } = {
|
||||
"api_credentials": SwapCreateStep.ApiKey,
|
||||
"o_auth2": SwapCreateStep.AuthorizeCoinbaseWithdrawal
|
||||
}
|
||||
|
||||
export const OfframpExchangeAuthorizationSteps: { [key: string]: SwapCreateStep } = {
|
||||
"api_credentials": SwapCreateStep.ApiKey,
|
||||
"o_auth2": SwapCreateStep.OffRampOAuth
|
||||
}
|
||||
|
||||
export class WizardStep<T> {
|
||||
Name: T;
|
||||
Content: FC;
|
||||
onBack?: () => void;
|
||||
onNext?: (data?: any) => Promise<void>;
|
||||
positionPercent: number;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Bridge App
|
||||
Up to date repository of Bridge UI App deployed at https://bridge.lux.network/app
|
||||
@@ -0,0 +1,23 @@
|
||||
import Jazzicon from "@metamask/jazzicon";
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
|
||||
type Props = {
|
||||
address: string;
|
||||
size: number;
|
||||
}
|
||||
const AddressIcon: FC<Props> = ({ address, size }) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (address && ref.current) {
|
||||
ref.current.innerHTML = "";
|
||||
const iconElement = Jazzicon(size, parseInt(address.slice(2, 10), 16)) as HTMLElement
|
||||
if(iconElement){
|
||||
iconElement.style.display = 'block'
|
||||
ref.current.appendChild(iconElement);
|
||||
}
|
||||
}
|
||||
}, [address, size]);
|
||||
|
||||
return <div ref={ref as any} />
|
||||
}
|
||||
export default AddressIcon
|
||||
@@ -0,0 +1,28 @@
|
||||
import Image from 'next/image'
|
||||
import { FC } from 'react'
|
||||
type Props = {
|
||||
imageUrls: string[]
|
||||
}
|
||||
|
||||
const AvatarGroup: FC<Props> = (({ imageUrls }) => {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="isolate flex -space-x-1 overflow-hidden p-1">
|
||||
{imageUrls.map(x => {
|
||||
return (
|
||||
<Image
|
||||
key={x}
|
||||
className="relative z-30 inline-block h-5 w-5 rounded-full ring-2 ring-secondary-600"
|
||||
src={x}
|
||||
width="60"
|
||||
height={60}
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
export default AvatarGroup;
|
||||
@@ -0,0 +1,200 @@
|
||||
import { FC, useState } from "react"
|
||||
import { useSettingsState } from "../../../context/settings"
|
||||
import Image from 'next/image'
|
||||
import { Trophy } from "lucide-react"
|
||||
import BridgeApiClient, { Campaign, Leaderboard, Reward } from "../../../lib/BridgeApiClient"
|
||||
import { RewardsComponentLeaderboardSceleton } from "../../Sceletons"
|
||||
import useSWR from "swr"
|
||||
import { ApiResponse } from "../../../Models/ApiResponse"
|
||||
import ClickTooltip from "../../Tooltips/ClickTooltip"
|
||||
import shortenAddress from "../../utils/ShortenAddress"
|
||||
import { useAccount } from "wagmi"
|
||||
import { truncateDecimals } from "../../utils/RoundDecimals"
|
||||
import AddressIcon from "../../AddressIcon";
|
||||
import Modal from "../../modal/modal";
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
campaign: Campaign
|
||||
}
|
||||
const Component: FC<Props> = ({ campaign }) => {
|
||||
const [openTopModal, setOpenTopModal] = useState(false)
|
||||
const settings = useSettingsState()
|
||||
const { address } = useAccount();
|
||||
|
||||
const handleOpenTopModal = () => {
|
||||
setOpenTopModal(true)
|
||||
}
|
||||
|
||||
const apiClient = new BridgeApiClient()
|
||||
const { data: leaderboardData, isLoading } = useSWR<ApiResponse<Leaderboard>>(`/campaigns/${campaign?.id}/leaderboard`, apiClient.fetcher, { dedupingInterval: 60000 })
|
||||
const { data: rewardsData, isLoading: rewardsIsLoading } = useSWR<ApiResponse<Reward>>(`/campaigns/${campaign.id}/rewards/${address}`, apiClient.fetcher, { dedupingInterval: 60000 })
|
||||
const leaderboard = leaderboardData?.data
|
||||
|
||||
if (isLoading) {
|
||||
return <RewardsComponentLeaderboardSceleton />
|
||||
}
|
||||
|
||||
if (!leaderboard) {
|
||||
//TODO handle
|
||||
return <></>
|
||||
}
|
||||
|
||||
const rewards = rewardsData?.data
|
||||
const { resolveImgSrc, networks, currencies } = settings
|
||||
const network = networks.find(n => n.internal_name === campaign?.network)
|
||||
const position = rewards?.user_reward.position || NaN
|
||||
const campaignAsset = currencies.find(c => c?.asset === campaign?.asset)
|
||||
|
||||
const leaderboardRewards = [
|
||||
leaderboard.leaderboard_budget * 0.6,
|
||||
leaderboard.leaderboard_budget * 0.3,
|
||||
leaderboard.leaderboard_budget * 0.1
|
||||
]
|
||||
|
||||
return <div className="space-y-2">
|
||||
{leaderboard?.leaderboard?.length > 0 &&
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-bold text-left leading-5">Leaderboard</p>
|
||||
<button onClick={handleOpenTopModal} type="button" className=" leading-4 text-base text-primary underline hover:no-underline hover:text-primary/80">
|
||||
Top 10
|
||||
</button>
|
||||
</div>}
|
||||
<p className="text-sm text-primary-text">Users who earn the most throughout the program will be featured here.</p>
|
||||
<div className="bg-secondary-700 border border-secondary-700 hover:border-secondary-500 transition duration-200 rounded-lg shadow-lg">
|
||||
<div className="p-3">
|
||||
{leaderboard?.leaderboard?.length > 0 ? <div className="space-y-6">
|
||||
{
|
||||
leaderboard?.leaderboard?.filter(u => u.position < 4).map(user => (
|
||||
<div key={user.position} className="items-center flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<p className="text-xl font-medium text-primary-text w-fit mr-1">{user.position}.</p>
|
||||
<div className="cols-start-2 flex items-center space-x-2">
|
||||
<AddressIcon address={user.address} size={25} />
|
||||
<div>
|
||||
<div className="text-sm font-bold text-primary-text leading-3">
|
||||
{user?.address && network?.account_explorer_template && <Link target="_blank" className="hover:opacity-80" href={network?.account_explorer_template?.replace("{0}", user?.address)}>
|
||||
{user?.position === rewards?.user_reward?.position ? <span className="text-primary">You</span> : shortenAddress(user?.address)}
|
||||
</Link>}
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-medium text-secondary-text leading-3">{truncateDecimals(user.amount, campaignAsset?.precision)} {campaign?.asset}</p>
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
{
|
||||
leaderboard.leaderboard_budget > 0 && <div className="text-right flex items-center space-x-2">
|
||||
<ClickTooltip text={
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>+</span>
|
||||
<div className="h-3.5 w-3.5 relative">
|
||||
<Image
|
||||
src={resolveImgSrc(campaign)}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-full object-contain" />
|
||||
</div>
|
||||
<p>
|
||||
<span>{leaderboardRewards[user.position - 1]} {campaign?.asset}</span>
|
||||
</p>
|
||||
</div>}>
|
||||
<div className='text-primary-text hover:cursor-pointer hover:text-primary-text ml-0.5 hover:bg-secondary-200 active:ring-2 active:ring-gray-200 active:bg-secondary-400 focus:outline-none cursor-default p-1 rounded'>
|
||||
<Trophy className="h-4 w-4" aria-hidden="true" />
|
||||
</div>
|
||||
</ClickTooltip>
|
||||
</div>
|
||||
}
|
||||
</div >
|
||||
))
|
||||
|
||||
}
|
||||
{
|
||||
position >= 4 && address && rewards?.user_reward &&
|
||||
<div className={position > 4 ? "!mt-0 !pt-0" : ""}>
|
||||
{position > 4 && < div className="text-2xl text-center leading-3 text-secondary-text my-3">
|
||||
...
|
||||
</div>}
|
||||
<div key={position} className="items-center flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<p className="text-xl font-medium text-primary-text w-fit mr-1">{position}.</p>
|
||||
<div className="cols-start-2 flex items-center space-x-2">
|
||||
<AddressIcon address={address} size={25} />
|
||||
<div>
|
||||
<div className="text-sm font-bold text-primary-text leading-3">
|
||||
{network?.account_explorer_template && <Link target="_blank" className="hover:opacity-80" href={network?.account_explorer_template?.replace("{0}", address)}>
|
||||
<span className="text-primary">You</span>
|
||||
</Link>}
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-medium text-secondary-text leading-3">{truncateDecimals(rewards.user_reward.total_amount, campaignAsset?.precision)} {campaign?.asset}</p>
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
</div >
|
||||
</div >
|
||||
}
|
||||
</div >
|
||||
:
|
||||
<div className="h-8 flex flex-col justify-center items-center text-sm">
|
||||
Here will be leaderboard.
|
||||
</div>
|
||||
}
|
||||
</div >
|
||||
</div >
|
||||
<Modal height="full" header='Leaderboard' show={openTopModal} setShow={setOpenTopModal} >
|
||||
<div className="bg-secondary-700 border border-secondary-700 mt-2 hover:border-secondary-500 transition duration-200 rounded-lg shadow-lg text-secondary-text">
|
||||
<div className="p-3">
|
||||
<div className="space-y-6">
|
||||
{
|
||||
leaderboard?.leaderboard?.map(user => (
|
||||
<div key={user.position} className="items-center flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<p className="text-xl font-medium text-primary-text w-fit mr-1">{user.position}.</p>
|
||||
<div className="cols-start-2 flex items-center space-x-2">
|
||||
<AddressIcon address={user.address} size={25} />
|
||||
<div>
|
||||
<div className="text-sm font-bold text-primary-text leading-3">
|
||||
{user?.address && network?.account_explorer_template && <Link target="_blank" className="hover:opacity-80" href={network?.account_explorer_template?.replace("{0}", user?.address)}>
|
||||
{user.position === rewards?.user_reward?.position ? <span className="text-primary">You</span> : shortenAddress(user.address)}
|
||||
</Link>}
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-medium text-secondary-text leading-3">{truncateDecimals(user.amount, campaignAsset?.precision)} {campaign?.asset}</p>
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
{
|
||||
user.position < 4 && leaderboard.leaderboard_budget > 0 &&
|
||||
<div className="text-right flex items-center space-x-2">
|
||||
<ClickTooltip text={
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>+</span>
|
||||
<div className="h-3.5 w-3.5 relative">
|
||||
<Image
|
||||
src={resolveImgSrc(campaign)}
|
||||
alt="Address Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-full object-contain" />
|
||||
</div>
|
||||
<p>
|
||||
<span>{leaderboardRewards[user.position - 1]} {campaign?.asset}</span>
|
||||
</p>
|
||||
</div>
|
||||
}>
|
||||
<div className='text-secondary-text hover:cursor-pointer hover:text-primary-text ml-0.5 hover:bg-secondary-200 active:ring-2 active:ring-gray-200 active:bg-secondary-400 focus:outline-none cursor-default p-1 rounded'>
|
||||
<Trophy className="h-4 w-4" aria-hidden="true" />
|
||||
</div>
|
||||
</ClickTooltip>
|
||||
</div>
|
||||
}
|
||||
</div >
|
||||
))
|
||||
}
|
||||
</div >
|
||||
</div >
|
||||
</div >
|
||||
</Modal >
|
||||
</div >
|
||||
}
|
||||
export default Component
|
||||
@@ -0,0 +1,175 @@
|
||||
|
||||
import { FC } from "react"
|
||||
import { useSettingsState } from "../../../context/settings"
|
||||
import Image from 'next/image'
|
||||
import BackgroundField from "../../backgroundField";
|
||||
import { Clock } from "lucide-react"
|
||||
import BridgeApiClient, { Campaign, Reward, RewardPayout } from "../../../lib/BridgeApiClient"
|
||||
import { RewardsComponentSceleton } from "../../Sceletons"
|
||||
import useSWR from "swr"
|
||||
import { ApiResponse } from "../../../Models/ApiResponse"
|
||||
import ClickTooltip from "../../Tooltips/ClickTooltip"
|
||||
import shortenAddress from "../../utils/ShortenAddress"
|
||||
import { useAccount } from "wagmi"
|
||||
import { Progress } from "../../ProgressBar";
|
||||
|
||||
type Props = {
|
||||
campaign: Campaign
|
||||
}
|
||||
|
||||
const Rewards: FC<Props> = ({ campaign }) => {
|
||||
const settings = useSettingsState()
|
||||
const { resolveImgSrc, networks, currencies } = settings
|
||||
const { address } = useAccount();
|
||||
const apiClient = new BridgeApiClient()
|
||||
|
||||
const { data: rewardsData, isLoading: rewardsIsLoading } = useSWR<ApiResponse<Reward>>(`/campaigns/${campaign.id}/rewards/${address}`, apiClient.fetcher, { dedupingInterval: 60000 })
|
||||
const { data: payoutsData, isLoading: payoutsIsLoading } = useSWR<ApiResponse<RewardPayout[]>>(`/campaigns/${campaign.id}/payouts/${address}`, apiClient.fetcher, { dedupingInterval: 60000 })
|
||||
|
||||
if (rewardsIsLoading || payoutsIsLoading) {
|
||||
return <RewardsComponentSceleton />
|
||||
}
|
||||
|
||||
const payouts = payoutsData?.data || []
|
||||
const totalBudget = campaign.total_budget
|
||||
|
||||
const network = networks.find(n => n.internal_name === campaign.network)
|
||||
const rewards = rewardsData?.data
|
||||
const campaignEndDate = new Date(campaign.end_date)
|
||||
const now = new Date()
|
||||
const next = rewards?.next_airdrop_date ? new Date(rewards?.next_airdrop_date) : null
|
||||
|
||||
const difference_in_days = next ?
|
||||
Math.floor(Math.abs(((next.getTime() - now.getTime())) / (1000 * 3600 * 24))) : null
|
||||
|
||||
const difference_in_hours = (next && difference_in_days) ?
|
||||
Math.round(Math.abs(((next.getTime() - now.getTime())) / (1000 * 3600) - (difference_in_days * 24)))
|
||||
: null
|
||||
|
||||
const campaignIsEnded = (campaignEndDate.getTime() - now.getTime()) < 0 || campaign.status !== 'active'
|
||||
|
||||
const DistributedAmount = ((campaign.distributed_amount / campaign.total_budget) * 100)
|
||||
const usdc_price = settings?.currencies?.find(c => c.asset === campaign.asset)?.usd_price
|
||||
const total_amount = rewards?.user_reward.total_amount
|
||||
const total_in_usd = (total_amount && usdc_price) ? (usdc_price * total_amount).toFixed(2) : null
|
||||
|
||||
return <>
|
||||
<div className="space-y-4">
|
||||
<p className="text-primary-text">
|
||||
<span>
|
||||
<span>Onboarding incentives that are earned by transferring to </span>{network?.display_name}<span>. </span>
|
||||
<a
|
||||
target='_blank'
|
||||
href="https://docs.bridge.lux.network/user-docs/bridge-campaigns/usdop-rewards"
|
||||
className="text-primary-text underline hover:no-underline decoration-white cursor-pointer"
|
||||
>Learn more</a>
|
||||
</span>
|
||||
</p>
|
||||
<div className="bg-secondary-700 divide-y divide-secondary-500 rounded-lg shadow-lg border border-secondary-700 hover:border-secondary-500 transition duration-200">
|
||||
{!campaignIsEnded &&
|
||||
<BackgroundField header={<span className="flex justify-between"><span className="flex items-center"><span>Pending Earnings </span><ClickTooltip text={`${campaign?.asset} tokens that will be airdropped periodically.`} /> </span><span>Next Airdrop</span></span>} withoutBorder>
|
||||
<div className="flex justify-between w-full text-2xl">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="h-5 w-5 relative">
|
||||
<Image
|
||||
src={resolveImgSrc(campaign)}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-full object-contain" />
|
||||
</div>
|
||||
<p>
|
||||
{rewards?.user_reward.total_pending_amount} <span className="text-base sm:text-2xl">{campaign?.asset}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="h-5" />
|
||||
<p>
|
||||
{difference_in_days}d {difference_in_hours}h
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</BackgroundField>
|
||||
}
|
||||
<BackgroundField header={<span className="flex justify-between"><span className="flex items-center"><span>Total Earnings </span><ClickTooltip text={`${campaign?.asset} tokens that you’ve earned so far (including Pending Earnings).`} /></span><span>Current Value</span></span>} withoutBorder>
|
||||
<div className="flex justify-between w-full text-slate-300 text-2xl">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="h-5 w-5 relative">
|
||||
<Image
|
||||
src={resolveImgSrc(campaign)}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-full object-contain" />
|
||||
</div>
|
||||
<p>
|
||||
{rewards?.user_reward.total_amount} <span className="text-base sm:text-2xl">{campaign?.asset}</span>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
${total_in_usd}
|
||||
</p>
|
||||
</div>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bg-secondary-700 rounded-lg shadow-lg border border-secondary-700 hover:border-secondary-500 transition duration-200">
|
||||
<BackgroundField header={
|
||||
<>
|
||||
<p className="flex items-center"><span>{campaign?.asset} pool</span>
|
||||
<ClickTooltip text={`The amount of ${campaign?.asset} to be distributed during this round of the campaign.`} />
|
||||
</p>
|
||||
</>
|
||||
} withoutBorder>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<Progress value={DistributedAmount === Infinity ? 0 : DistributedAmount} />
|
||||
<div className="flex justify-between w-full font-semibold text-sm ">
|
||||
<div className="text-primary"><span className="text-primary-text">{campaign?.distributed_amount.toFixed(0)}</span> <span>/</span> {totalBudget} {campaign?.asset}</div>
|
||||
</div>
|
||||
</div>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
{
|
||||
payouts.length > 0 &&
|
||||
<div className="space-y-1">
|
||||
<p className="font-bold text-lg text-left">Payouts</p>
|
||||
<div className=" bg-secondary-700 divide-y divide-secondary-300 rounded-lg shadow-lg border border-secondary-700 hover:border-secondary-500 transition duration-200">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-secondary-500">
|
||||
<thead className="bg-secondary-800/70">
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 text-left text-sm font-semibold sm:pl-6">
|
||||
Tx Id
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold ">
|
||||
Amount
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold ">
|
||||
Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-secondary-600">
|
||||
{payouts.map((payout) => (
|
||||
<tr key={payout.transaction_id}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-primary-text sm:pl-6 underline hover:no-underline">
|
||||
<a target={"_blank"} href={network?.transaction_explorer_template?.replace("{0}", payout.transaction_id)}>{shortenAddress(payout.transaction_id)}</a>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-100">{payout.amount}</td>
|
||||
<td className="px-3 py-4 text-sm text-gray-100">{new Date(payout.date).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
export default Rewards
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useRouter } from "next/router"
|
||||
import { FC } from "react"
|
||||
import { useSettingsState } from "../../../context/settings"
|
||||
import Image from 'next/image'
|
||||
import { Gift } from "lucide-react"
|
||||
import BridgeApiClient, { Campaign } from "../../../lib/BridgeApiClient"
|
||||
import useSWR from "swr"
|
||||
import { ApiResponse } from "../../../Models/ApiResponse"
|
||||
import { useAccount } from "wagmi"
|
||||
import RainbowKit from "../../Swap/Withdraw/Wallet/RainbowKit"
|
||||
import SubmitButton from "../../buttons/submitButton";
|
||||
import WalletIcon from "../../icons/WalletIcon";
|
||||
import Link from "next/link";
|
||||
import LinkWrapper from "../../LinkWraapper";
|
||||
import { Widget } from "../../Widget/Index";
|
||||
import Leaderboard from "./Leaderboard"
|
||||
import { CryptoNetwork } from "../../../Models/CryptoNetwork";
|
||||
import Rewards from "./Rewards";
|
||||
import SpinIcon from "../../icons/spinIcon"
|
||||
|
||||
function CampaignDetails() {
|
||||
|
||||
const settings = useSettingsState()
|
||||
const router = useRouter();
|
||||
const { resolveImgSrc, networks } = settings
|
||||
const camapaignName = router.query.campaign?.toString()
|
||||
|
||||
const { isConnected } = useAccount();
|
||||
|
||||
const apiClient = new BridgeApiClient()
|
||||
const { data: campaignsData, isLoading } = useSWR<ApiResponse<Campaign[]>>('/campaigns', apiClient.fetcher)
|
||||
const campaign = campaignsData?.data?.find(c => c.name === camapaignName)
|
||||
|
||||
const network = networks.find(n => n.internal_name === campaign?.network)
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
if (!campaign) {
|
||||
return <NotFound />
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Widget.Content>
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-7 w-7 relative">
|
||||
{network && <Image
|
||||
src={resolveImgSrc(network)}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-md object-contain" />}
|
||||
</div>
|
||||
<p className="font-bold text-xl text-left flex items-center">
|
||||
{network?.display_name} Rewards
|
||||
</p>
|
||||
</div>
|
||||
{
|
||||
isConnected ?
|
||||
<Rewards
|
||||
campaign={campaign}
|
||||
/>
|
||||
:
|
||||
<BriefInformation
|
||||
network={network}
|
||||
campaign={campaign}
|
||||
/>
|
||||
}
|
||||
<Leaderboard campaign={campaign} />
|
||||
</div>
|
||||
</Widget.Content>
|
||||
<>
|
||||
{
|
||||
!isConnected &&
|
||||
<Widget.Footer>
|
||||
<RainbowKit>
|
||||
<SubmitButton isDisabled={false} isSubmitting={false} icon={<WalletIcon className="stroke-2 w-6 h-6" />}>
|
||||
Connect a wallet
|
||||
</SubmitButton>
|
||||
</RainbowKit>
|
||||
</Widget.Footer>
|
||||
}
|
||||
</>
|
||||
</Widget >
|
||||
)
|
||||
}
|
||||
|
||||
type BriefInformationProps = {
|
||||
campaign: Campaign,
|
||||
network?: CryptoNetwork
|
||||
}
|
||||
const BriefInformation: FC<BriefInformationProps> = ({ campaign, network }) =>
|
||||
<p className="text-secondary-text text-base">
|
||||
<span>You can earn $</span>
|
||||
<span>{campaign?.asset}</span>
|
||||
<span> tokens by transferring assets to </span>
|
||||
<span>{network?.display_name || campaign.network}</span>
|
||||
<span>. For each transaction, you&ll receive </span>
|
||||
<span>{campaign?.percentage}</span>
|
||||
<span>% of Bridge fee back. </span>
|
||||
<Link target='_blank' href="https://docs.bridge.lux.network/user-docs/bridge-campaigns/usdop-rewards"
|
||||
className="text-primary underline hover:no-underline decoration-primary cursor-pointer">
|
||||
Learn more
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
const Loading = () => <Widget className="min-h-[500px]">
|
||||
<Widget.Content>
|
||||
<div className="absolute top-[calc(50%-5px)] left-[calc(50%-5px)]">
|
||||
<SpinIcon className="animate-spin h-5 w-5" />
|
||||
</div>
|
||||
</Widget.Content>
|
||||
</Widget>
|
||||
|
||||
const NotFound = () => <Widget className="min-h-[500px]">
|
||||
<Widget.Content>
|
||||
<div className="h-[364px] flex flex-col items-center justify-center space-y-4">
|
||||
<Gift className="h-20 w-20 text-primary" />
|
||||
<p className="font-bold text-center">Campaign not found</p>
|
||||
<LinkWrapper className="text-xs underline hover:no-underline" href='/campaigns'>See all campaigns</LinkWrapper>
|
||||
</div>
|
||||
</Widget.Content>
|
||||
</Widget>
|
||||
|
||||
export default CampaignDetails;
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Gift } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { FC, useCallback } from "react";
|
||||
import { ApiResponse } from "../../Models/ApiResponse";
|
||||
import BridgeApiClient, { Campaign } from "../../lib/BridgeApiClient";
|
||||
import SpinIcon from "../icons/spinIcon";
|
||||
import useSWR from 'swr'
|
||||
import { useSettingsState } from "../../context/settings";
|
||||
import Image from "next/image";
|
||||
import LinkWrapper from "../LinkWraapper";
|
||||
import { Layer } from "../../Models/Layer";
|
||||
import { Widget } from "../Widget/Index";
|
||||
|
||||
const Rewards = () => {
|
||||
|
||||
const { layers, resolveImgSrc } = useSettingsState()
|
||||
const apiClient = new BridgeApiClient()
|
||||
const { data: campaignsData, isLoading } = useSWR<ApiResponse<Campaign[]>>('/campaigns', apiClient.fetcher)
|
||||
const campaigns = campaignsData?.data
|
||||
|
||||
const activeCampaigns = campaigns?.filter(IsCampaignActive) || []
|
||||
const inactiveCampaigns = campaigns?.filter(c => !IsCampaignActive(c)) || []
|
||||
|
||||
return (
|
||||
<Widget className="min-h-[520px]">
|
||||
<Widget.Content>
|
||||
{!isLoading ?
|
||||
<div className="space-y-5 h-full text-primary-text">
|
||||
<div className="space-y-2">
|
||||
<p className="font-bold text-left leading-5">Campaigns</p>
|
||||
<div className="bg-secondary-700 border border-secondary-700 hover:border-secondary-500 transition duration-200 rounded-lg shadow-lg">
|
||||
<div className="p-3 space-y-4">
|
||||
{
|
||||
activeCampaigns.length > 0 ?
|
||||
activeCampaigns.map(c =>
|
||||
<CampaignItem
|
||||
campaign={c}
|
||||
layers={layers}
|
||||
resolveImgSrc={resolveImgSrc}
|
||||
key={c.id}
|
||||
/>)
|
||||
:
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<Gift className="h-10 w-10 text-primary" />
|
||||
<p className="font-bold text-center">There are no active campaigns right now</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
inactiveCampaigns.length > 0 &&
|
||||
<div className="space-y-2">
|
||||
<p className="font-bold text-left leading-5">Old campaigns</p>
|
||||
<div className="bg-secondary-700 border border-secondary-700 hover:border-secondary-500 transition duration-200 rounded-lg shadow-lg">
|
||||
<div className="p-3 dpsv flex flex-col space-y-4">
|
||||
{inactiveCampaigns.map(c =>
|
||||
<CampaignItem
|
||||
campaign={c}
|
||||
layers={layers}
|
||||
resolveImgSrc={resolveImgSrc}
|
||||
key={c.id}
|
||||
/>)}
|
||||
</div >
|
||||
</div >
|
||||
</div >
|
||||
}
|
||||
</div >
|
||||
:
|
||||
<div className="absolute top-[calc(50%-5px)] left-[calc(50%-5px)]">
|
||||
<SpinIcon className="animate-spin h-5 w-5" />
|
||||
</div>
|
||||
}
|
||||
</Widget.Content>
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
type CampaignProps = {
|
||||
campaign: Campaign,
|
||||
layers: Layer[],
|
||||
resolveImgSrc: (item: Layer) => string
|
||||
}
|
||||
const CampaignItem: FC<CampaignProps> = ({ campaign, layers, resolveImgSrc }) => {
|
||||
|
||||
const campaignLayer = layers.find(l => l.internal_name === campaign.network)
|
||||
const campaignDaysLeft = ((new Date(campaign.end_date).getTime() - new Date().getTime()) / 86400000).toFixed()
|
||||
const campaignIsActive = IsCampaignActive(campaign)
|
||||
|
||||
return <LinkWrapper href={`/campaigns/${campaign.name}`}
|
||||
className="flex justify-between items-center">
|
||||
<span className="flex items-center gap-1 hover:opacity-70 active:scale-90 duration-200 transition-all">
|
||||
<span className="h-5 w-5 relative">
|
||||
{campaignLayer && <Image
|
||||
src={resolveImgSrc(campaignLayer)}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-md object-contain" />}
|
||||
</span>
|
||||
<span className="font-semibold text-base text-left flex items-center">{campaign?.display_name} </span>
|
||||
</span>
|
||||
{
|
||||
campaignIsActive &&
|
||||
<span className="text-primary-text-muted text-right text-sm">
|
||||
{campaignDaysLeft} days left
|
||||
</span>
|
||||
}
|
||||
</LinkWrapper>
|
||||
}
|
||||
|
||||
function IsCampaignActive(campaign: Campaign) {
|
||||
const now = new Date()
|
||||
return campaign.status == 'active' && (new Date(campaign?.end_date).getTime() > now.getTime())
|
||||
}
|
||||
|
||||
export default Rewards
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { useSwipeable } from "react-swipeable";
|
||||
|
||||
|
||||
interface CarouselItemProps {
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const CarouselItem: React.FC<CarouselItemProps> = ({ children, width }) => {
|
||||
return (
|
||||
<div className={`rounded-xl inline-flex items-center justify-center flex-col pb-0 bg-gradient-to-b from-secondary-900 to-secondary-700 h-full relative`} style={{ width: width }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CarouselProps {
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
starAtLast: boolean;
|
||||
onLast: (value: boolean) => void;
|
||||
onFirst: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export type CarouselRef = {
|
||||
next: () => void;
|
||||
prev: () => void;
|
||||
goToLast: () => void;
|
||||
goToFirst: () => void;
|
||||
};
|
||||
|
||||
const Carousel = forwardRef<CarouselRef, CarouselProps>(function Carousel({ onFirst, onLast, children, starAtLast }, ref) {
|
||||
const [activeIndex, setActiveIndex] = useState(starAtLast ? React.Children.count(children) - 1 : 0);
|
||||
|
||||
const updateIndex = useCallback((newIndex) => {
|
||||
onFirst(false)
|
||||
onLast(false)
|
||||
if (newIndex >= 0 && newIndex <= React.Children.count(children) - 1) {
|
||||
setActiveIndex(newIndex);
|
||||
}
|
||||
if (newIndex >= React.Children.count(children) - 1)
|
||||
onLast(true)
|
||||
if (newIndex == 0)
|
||||
onFirst(true)
|
||||
}, [children, onFirst, onLast]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
next: () => {
|
||||
updateIndex(activeIndex + 1)
|
||||
},
|
||||
prev: () => {
|
||||
updateIndex(activeIndex - 1);
|
||||
},
|
||||
goToLast: () => {
|
||||
updateIndex(React.Children.count(children) - 1);
|
||||
},
|
||||
goToFirst: () => {
|
||||
updateIndex(0);
|
||||
}
|
||||
}), [activeIndex, children, updateIndex]);
|
||||
|
||||
const handlers = useSwipeable({
|
||||
onSwipedLeft: () => updateIndex(activeIndex + 1),
|
||||
onSwipedRight: () => updateIndex(activeIndex - 1),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...handlers}
|
||||
className="overflow-hidden h-full"
|
||||
>
|
||||
<div
|
||||
className="whitespace-nowrap transition-transform duration-500 inner h-full"
|
||||
style={{ transform: `translateX(-${activeIndex * 100}%)` }}
|
||||
>
|
||||
{children && React.Children.map(children, (child, index) => {
|
||||
return React.cloneElement(child, { width: "100%" });
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Carousel;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { FC } from "react";
|
||||
import { THEME_COLORS, ThemeData } from "../Models/Theme";
|
||||
|
||||
type Props = {
|
||||
themeData?: ThemeData | null
|
||||
}
|
||||
const ColorSchema: FC<Props> = ({ themeData }) => {
|
||||
themeData = themeData || THEME_COLORS.default
|
||||
return (
|
||||
<>
|
||||
{themeData &&
|
||||
<style global jsx>{`
|
||||
:root {
|
||||
--ls-colors-backdrop:${themeData.backdrop};
|
||||
--ls-colors-logo: ${themeData.logo};
|
||||
--ls-colors-primary: ${themeData.primary?.DEFAULT};
|
||||
--ls-colors-primary-50: ${themeData.primary?.[50]};
|
||||
--ls-colors-primary-100: ${themeData.primary?.[100]};
|
||||
--ls-colors-primary-200: ${themeData.primary?.[200]};
|
||||
--ls-colors-primary-300: ${themeData.primary?.[300]};
|
||||
--ls-colors-primary-400: ${themeData.primary?.[400]};
|
||||
--ls-colors-primary-500: ${themeData.primary?.[500]};
|
||||
--ls-colors-primary-600: ${themeData.primary?.[600]};
|
||||
--ls-colors-primary-700: ${themeData.primary?.[700]};
|
||||
--ls-colors-primary-800: ${themeData.primary?.[800]};
|
||||
--ls-colors-primary-900: ${themeData.primary?.[900]};
|
||||
|
||||
--ls-colors-actionButtonText: ${themeData.actionButtonText};
|
||||
--ls-colors-text-placeholder: ${themeData.placeholderText};
|
||||
--ls-colors-primary-text: ${themeData.primary?.text};
|
||||
--ls-colors-primary-text-muted: ${themeData.primary?.textMuted};
|
||||
--ls-colors-primary-logoColor: ${themeData.logo};
|
||||
|
||||
--ls-colors-secondary: ${themeData.secondary?.DEFAULT};
|
||||
--ls-colors-secondary-50: ${themeData.secondary?.[50]};
|
||||
--ls-colors-secondary-100: ${themeData.secondary?.[100]};
|
||||
--ls-colors-secondary-200: ${themeData.secondary?.[200]};
|
||||
--ls-colors-secondary-300: ${themeData.secondary?.[300]};
|
||||
--ls-colors-secondary-400: ${themeData.secondary?.[400]};
|
||||
--ls-colors-secondary-500: ${themeData.secondary?.[500]};
|
||||
--ls-colors-secondary-600: ${themeData.secondary?.[600]};
|
||||
--ls-colors-secondary-700: ${themeData.secondary?.[700]};
|
||||
--ls-colors-secondary-800: ${themeData.secondary?.[800]};
|
||||
--ls-colors-secondary-900: ${themeData.secondary?.[900]};
|
||||
--ls-colors-secondary-950: ${themeData.secondary?.[950]};
|
||||
--ls-colors-secondary-text: ${themeData.secondary?.text};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ColorSchema
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FC } from "react";
|
||||
|
||||
type AverageCompletionTimeProps = {
|
||||
time: string | undefined
|
||||
}
|
||||
|
||||
const AverageCompletionTime: FC<AverageCompletionTimeProps> = ({ time }) => {
|
||||
|
||||
if (!time) {
|
||||
return <span>~1-2 minutes</span>
|
||||
}
|
||||
|
||||
const parts = time?.split(":");
|
||||
const averageTimeInMinutes = parts && parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10) + parseInt(parts[2]) / 60
|
||||
|
||||
const hours = Math.floor(averageTimeInMinutes / 60);
|
||||
const minutes = averageTimeInMinutes % 60;
|
||||
|
||||
if (averageTimeInMinutes > 1 && averageTimeInMinutes < 60) return <span><span>~</span><span>{averageTimeInMinutes.toFixed()}</span> <span>minutes</span></span>
|
||||
else if (averageTimeInMinutes >= 60) return <span><span>~</span><span>{hours}</span> <span>{hours > 1 ? 'hours' : 'hour'}</span> <span>{minutes > 0 ? ` ${minutes?.toFixed()} minutes` : ''}</span></span>
|
||||
else return <span>~1-2 minutes</span>
|
||||
}
|
||||
|
||||
export default AverageCompletionTime
|
||||
@@ -0,0 +1,12 @@
|
||||
const FormattedDate = ({ date }: { date: string }) => {
|
||||
const swapDate = new Date(date);
|
||||
const yyyy = swapDate.getFullYear();
|
||||
let mm = swapDate.getMonth() + 1; // Months start at 0!
|
||||
let dd = swapDate.getDate();
|
||||
|
||||
if (dd < 10) dd = 0 + dd;
|
||||
if (mm < 10) mm = 0 + mm;
|
||||
|
||||
return <>{dd + '/' + mm + '/' + yyyy}</>;
|
||||
}
|
||||
export default FormattedDate
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
type Props = {
|
||||
wrapperId: string;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const ReactPortal: FC<Props> = ({ children, wrapperId = "react-portal-wrapper" }) => {
|
||||
const ref = useRef<Element | null>(null);
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let element = document.getElementById(wrapperId);
|
||||
if (!element) {
|
||||
element = document.createElement('div');
|
||||
element.setAttribute("id", wrapperId);
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
ref.current = element
|
||||
setMounted(true)
|
||||
}, [wrapperId]);
|
||||
|
||||
return ref.current && mounted ? createPortal(children, ref.current) : null;
|
||||
};
|
||||
|
||||
export default ReactPortal
|
||||
@@ -0,0 +1,32 @@
|
||||
import SubmitButton from './buttons/submitButton';
|
||||
import { Link } from 'lucide-react';
|
||||
import { FC } from 'react';
|
||||
|
||||
type Props = {
|
||||
NetworkDisplayName: string;
|
||||
AppURL: string;
|
||||
}
|
||||
|
||||
const ConnectNetwork: FC<Props> = ({ NetworkDisplayName, AppURL }) => {
|
||||
const connectButtonIcon = <Link className='h-5 w-5'></Link>
|
||||
|
||||
return (
|
||||
<div className="relative inset-0 flex flex-col overflow-y-auto styled-scroll">
|
||||
<div className="relative min-h-full items-center justify-center pt-0 text-center">
|
||||
<h3 className='mb-4 pt-2 text-xl text-center md:text-left font-roboto text-secondary-text font-semibold'>
|
||||
<p className='mb-10 pt-2 text-base text-center md:text-left font-roboto text-pink-primary-300 font-light'>
|
||||
{NetworkDisplayName} account with the provided address does not exist. To create one, go to {NetworkDisplayName} and connect your wallet.
|
||||
</p>
|
||||
</h3>
|
||||
|
||||
<div className="mt-3 sm:mt-6 text-primary-text text-sm">
|
||||
<SubmitButton icon={connectButtonIcon} isDisabled={false} isSubmitting={false} onClick={() => window.open(AppURL, '_blank')}>
|
||||
Connect
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConnectNetwork;
|
||||
@@ -0,0 +1,148 @@
|
||||
import WalletIcon from "./icons/WalletIcon"
|
||||
import shortenAddress from "./utils/ShortenAddress"
|
||||
import useWallet from "../hooks/useWallet"
|
||||
import ConnectButton from "./buttons/connectButton"
|
||||
import SubmitButton from "./buttons/submitButton"
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "./shadcn/dialog"
|
||||
import { useState } from "react"
|
||||
import { Plus } from "lucide-react"
|
||||
import AddressIcon from "./AddressIcon"
|
||||
import { Wallet } from "../stores/walletStore"
|
||||
|
||||
export const WalletsHeader = () => {
|
||||
const { wallets } = useWallet()
|
||||
const [openDialog, setOpenDialog] = useState<boolean>(false)
|
||||
|
||||
if (wallets.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<button type="button" aria-label="Connected wallets" onClick={() => setOpenDialog(true)} className="-mx-2 p-1.5 justify-self-start text-secondary-text hover:bg-secondary-500 hover:text-primary-text focus:outline-none inline-flex rounded-lg items-center">
|
||||
<WalletsIcons wallets={wallets} />
|
||||
</button>
|
||||
<ConnectedWalletsDialog openDialog={openDialog} setOpenDialog={setOpenDialog} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectButton>
|
||||
<div className="-mx-2 p-1.5 justify-self-start text-secondary-text hover:bg-secondary-500 hover:text-primary-text focus:outline-none inline-flex rounded-lg items-center">
|
||||
<WalletIcon className="h-6 w-6 mx-0.5" strokeWidth="2" />
|
||||
</div>
|
||||
</ConnectButton>
|
||||
)
|
||||
}
|
||||
|
||||
const WalletsIcons = ({ wallets }: { wallets: Wallet[] }) => {
|
||||
const firstWallet = wallets[0]
|
||||
const secondWallet = wallets[1]
|
||||
|
||||
return (
|
||||
<div className="-space-x-2 flex">
|
||||
{
|
||||
firstWallet?.connector &&
|
||||
<firstWallet.icon className="rounded-full border-2 border-secondary-600 bg-secondary-700 flex-shrink-0 h-6 w-6" />
|
||||
}
|
||||
{
|
||||
secondWallet?.connector &&
|
||||
<secondWallet.icon className="rounded-full border-2 border-secondary-600 bg-secondary-700 flex-shrink-0 h-6 w-6" />
|
||||
}
|
||||
{
|
||||
wallets.length > 2 &&
|
||||
<div className="h-6 w-6 flex-shrink-0 rounded-full justify-center p-1 bg-secondary-600 text-primary-text overlfow-hidden text-xs">
|
||||
<span><span>+</span>{wallets.length - 2}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const WalletsMenu = () => {
|
||||
const [openDialog, setOpenDialog] = useState<boolean>(false)
|
||||
const { wallets } = useWallet()
|
||||
const wallet = wallets[0]
|
||||
if (wallets.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpenDialog(true)} type="button" className="py-3 px-4 bg-secondary-700 flex items-center w-full rounded-md space-x-1 disabled:text-opacity-40 disabled:bg-primary-900 disabled:cursor-not-allowed relative font-semibold transform border border-secondary-500 hover:bg-secondary-600 transition duration-200 ease-in-out">
|
||||
{
|
||||
wallets.length === 1 ?
|
||||
<div className="flex gap-4 items-start">
|
||||
<div className="inline-flex items-center relative">
|
||||
<AddressIcon address={wallet.address} size={20} />
|
||||
{
|
||||
wallet.connector && <span className="absolute -bottom-1 -right-2 ml-1 text-[10px] leading-4 font-semibold text-primary-text">
|
||||
<wallet.icon className="w-4 h-4 border-2 border-secondary-600 rounded-full bg-primary-text" />
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<p>{shortenAddress(wallet.address)}</p>
|
||||
</div>
|
||||
:
|
||||
<>
|
||||
<div className="flex justify-center w-full">
|
||||
Connected wallets
|
||||
</div>
|
||||
<div className="place-items-end absolute left-2.5">
|
||||
<WalletsIcons wallets={wallets} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</button>
|
||||
<ConnectedWalletsDialog openDialog={openDialog} setOpenDialog={setOpenDialog} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectButton>
|
||||
<SubmitButton text_align="center" icon={<WalletIcon className='h-5 w-5' strokeWidth={2} />} className="bg-primary/20 border-none !text-primary !px-4" type="button" isDisabled={false} isSubmitting={false}>
|
||||
Connect a wallet
|
||||
</SubmitButton>
|
||||
</ConnectButton>
|
||||
)
|
||||
}
|
||||
|
||||
const ConnectedWalletsDialog = ({ openDialog, setOpenDialog }: { openDialog: boolean, setOpenDialog: (open: boolean) => void }) => {
|
||||
const { wallets, disconnectWallet } = useWallet()
|
||||
|
||||
return (
|
||||
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">Wallets</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col justify-start space-y-2">
|
||||
{
|
||||
wallets.map((wallet, index) => (
|
||||
<div key={index} className="w-full relative items-center justify-between gap-2 flex rounded-md outline-none bg-secondary-700 text-primary-text p-3 border border-secondary-500 ">
|
||||
<div className="flex space-x-4 items-center">
|
||||
{
|
||||
wallet.connector &&
|
||||
<div className="inline-flex items-center relative">
|
||||
<wallet.icon className="w-8 h-8 p-0.5 rounded-full bg-secondary-800 border border-secondary-400" />
|
||||
</div>
|
||||
}
|
||||
<p>{shortenAddress(wallet.address)}</p>
|
||||
</div>
|
||||
<button onClick={() => { disconnectWallet(wallet.providerName); wallets.length === 1 && setOpenDialog(false) }} className="p-1 hover:bg-secondary-700 text-xs text-secondary-text hover:opacity-75">
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<ConnectButton onClose={() => setOpenDialog(false)}>
|
||||
<div className="text-secondary-text hover:text-secondary-text/80 flex items-center gap-1 justify-end w-fit">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="text-sm">
|
||||
Link a new wallet
|
||||
</span>
|
||||
</div>
|
||||
</ConnectButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FC } from "react"
|
||||
import { useIntercom } from "react-use-intercom"
|
||||
import { useAuthState } from "../context/authContext"
|
||||
|
||||
const ContactSupport: FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const { email, userId } = useAuthState()
|
||||
const { boot, show, update } = useIntercom()
|
||||
const updateWithProps = () => update({ email: email, userId: userId })
|
||||
|
||||
return <span onClick={() => {
|
||||
boot();
|
||||
show();
|
||||
updateWithProps()
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
}
|
||||
export default ContactSupport
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface SwapConfirmationFormValues {
|
||||
RightWallet: boolean;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Currency } from "../../Models/Currency";
|
||||
import { Layer } from "../../Models/Layer";
|
||||
|
||||
|
||||
export type SwapFormValues = {
|
||||
amount?: string;
|
||||
destination_address?: string;
|
||||
currency?: Currency;
|
||||
refuel?: boolean;
|
||||
from?: Layer;
|
||||
to?: Layer;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FC } from "react"
|
||||
import { Currency } from "../../../Models/Currency"
|
||||
import { Layer } from "../../../Models/Layer"
|
||||
import BridgeApiClient, { Campaign } from "../../../lib/BridgeApiClient"
|
||||
import useSWR from "swr"
|
||||
import { ApiResponse } from "../../../Models/ApiResponse"
|
||||
import { useSettingsState } from "../../../context/settings"
|
||||
import { truncateDecimals } from "../../utils/RoundDecimals"
|
||||
import { motion } from "framer-motion"
|
||||
import ClickTooltip from "../../Tooltips/ClickTooltip"
|
||||
import Image from 'next/image';
|
||||
|
||||
type CampaignProps = {
|
||||
destination: Layer,
|
||||
fee: number,
|
||||
selected_currency: Currency,
|
||||
}
|
||||
const Campaign: FC<CampaignProps> = ({
|
||||
destination,
|
||||
fee,
|
||||
selected_currency
|
||||
}) => {
|
||||
const apiClient = new BridgeApiClient()
|
||||
const { data: campaignsData } = useSWR<ApiResponse<Campaign[]>>('/campaigns', apiClient.fetcher)
|
||||
|
||||
const now = new Date().getTime()
|
||||
|
||||
const campaign = campaignsData
|
||||
?.data
|
||||
?.find(c =>
|
||||
c?.network === destination?.internal_name
|
||||
&& c.status == 'active'
|
||||
&& new Date(c?.end_date).getTime() - now > 0)
|
||||
|
||||
if (!campaign)
|
||||
return <></>
|
||||
|
||||
return <CampaignDisplay
|
||||
campaign={campaign}
|
||||
fee={fee}
|
||||
selected_currency={selected_currency}
|
||||
/>
|
||||
}
|
||||
type CampaignDisplayProps = {
|
||||
campaign: Campaign,
|
||||
fee: number,
|
||||
selected_currency: Currency,
|
||||
}
|
||||
const CampaignDisplay: FC<CampaignDisplayProps> = ({ campaign, fee, selected_currency }) => {
|
||||
const { resolveImgSrc, currencies } = useSettingsState()
|
||||
const campaignAsset = currencies.find(c => c?.asset === campaign?.asset)
|
||||
const feeinUsd = fee * selected_currency.usd_price
|
||||
const reward = truncateDecimals(((feeinUsd * (campaign?.percentage || 0) / 100) / (campaignAsset?.usd_price || 1)), (campaignAsset?.precision || 0))
|
||||
|
||||
return <motion.div
|
||||
initial={{ y: "-100%" }}
|
||||
animate={{
|
||||
y: 0,
|
||||
transition: { duration: 0.3, ease: [0.36, 0.66, 0.04, 1] },
|
||||
}}
|
||||
exit={{
|
||||
y: "-100%",
|
||||
transition: { duration: 0.4, ease: [0.36, 0.66, 0.04, 1] },
|
||||
}}
|
||||
className='w-full flex items-center justify-between rounded-b-lg bg-secondary-700 relative bottom-2 z-0 pt-4 pb-2 px-3.5 text-right'>
|
||||
<div className='flex items-center'>
|
||||
<p>Est. {campaignAsset?.asset} Reward</p>
|
||||
<ClickTooltip text={<span><span>The amount of onboarding reward that you’ll earn. </span><a target='_blank' href='/campaigns' className='text-primary underline hover:no-underline decoration-primary cursor-pointer'>Learn more</a></span>} />
|
||||
</div>
|
||||
{
|
||||
Number(reward) > 0 &&
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>+</span>
|
||||
<div className="h-5 w-5 relative">
|
||||
<Image
|
||||
src={resolveImgSrc(campaign)}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-md object-contain" />
|
||||
</div>
|
||||
<p>
|
||||
{reward} {campaignAsset?.asset}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</motion.div>
|
||||
}
|
||||
|
||||
export default Campaign
|
||||
@@ -0,0 +1,97 @@
|
||||
import { FC } from "react";
|
||||
import { CryptoNetwork } from "../../../Models/CryptoNetwork";
|
||||
import { Currency } from "../../../Models/Currency";
|
||||
import { Layer } from "../../../Models/Layer";
|
||||
import { truncateDecimals } from "../../utils/RoundDecimals";
|
||||
import { GetDefaultNetwork, GetNetworkCurrency } from "../../../helpers/settingsHelper";
|
||||
import AverageCompletionTime from "../../Common/AverageCompletionTime";
|
||||
import { useBalancesState } from "../../../context/balances";
|
||||
|
||||
type EstimatesProps = {
|
||||
networks: CryptoNetwork[]
|
||||
source?: Layer | null,
|
||||
destination?: Layer | null,
|
||||
selected_currency?: Currency | null,
|
||||
currencies: Currency[],
|
||||
fee: number
|
||||
}
|
||||
const DetailedEstimates: FC<EstimatesProps> = ({
|
||||
currencies,
|
||||
source,
|
||||
destination,
|
||||
selected_currency,
|
||||
fee }) => {
|
||||
|
||||
const parsedFee = parseFloat(fee.toFixed(selected_currency?.precision))
|
||||
const currencyName = selected_currency?.asset || " "
|
||||
|
||||
return <>
|
||||
<div className="mt-2 flex flex-row items-baseline justify-between">
|
||||
<label className="inline-flex items-center text-left text-primary-text-placeholder">
|
||||
Bridge fee
|
||||
</label>
|
||||
<div className="text-right">
|
||||
<span>{parsedFee}</span> <span>{currencyName}</span>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
source
|
||||
&& source?.isExchange === false
|
||||
&& selected_currency &&
|
||||
<NetworkGas network={source} currencies={currencies} selected_currency={selected_currency} />
|
||||
}
|
||||
<EstimatedArrival currency={selected_currency} destination={destination} />
|
||||
</>
|
||||
}
|
||||
type NetworkGasProps = {
|
||||
network: Layer & { isExchange: false },
|
||||
selected_currency: Currency,
|
||||
currencies: Currency[]
|
||||
}
|
||||
const NetworkGas: FC<NetworkGasProps> = ({ selected_currency, network, currencies }) => {
|
||||
|
||||
const { gases, isGasLoading } = useBalancesState()
|
||||
const networkGas = network.internal_name ?
|
||||
gases?.[network.internal_name]?.find(g => g?.token === selected_currency.asset)?.gas : null
|
||||
|
||||
if (!networkGas)
|
||||
return <></>
|
||||
|
||||
const source_native_currnecy = currencies.find(a => a.asset === network.native_currency)
|
||||
|
||||
const estimatedGas = (networkGas && source_native_currnecy) ?
|
||||
truncateDecimals(networkGas, source_native_currnecy?.precision)
|
||||
: truncateDecimals(networkGas, selected_currency?.precision)
|
||||
|
||||
return <div className="mt-2 flex flex-row items-baseline justify-between">
|
||||
<label className="inline-flex items-center text-left text-primary-text-placeholder">
|
||||
Estimated gas
|
||||
</label>
|
||||
<div className="text-right flex items-center gap-1">
|
||||
{isGasLoading ? <div className='h-[10px] w-10 bg-gray-500 rounded-sm animate-pulse' /> : estimatedGas} <span>{network?.native_currency ?? selected_currency.asset}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type EstimatedArrivalProps = {
|
||||
destination?: Layer | null,
|
||||
currency?: Currency | null
|
||||
}
|
||||
const EstimatedArrival: FC<EstimatedArrivalProps> = ({ currency, destination }) => {
|
||||
const destinationNetworkCurrency = (destination && currency) ? GetNetworkCurrency(destination, currency.asset) : null
|
||||
const destinationNetwork = GetDefaultNetwork(destination, currency?.asset)
|
||||
|
||||
return <div className="mt-2 flex flex-row items-baseline justify-between">
|
||||
<label className="block text-left text-primary-text-placeholder">
|
||||
Estimated arrival
|
||||
</label>
|
||||
<span className="text-right">
|
||||
{
|
||||
destinationNetworkCurrency?.status == 'insufficient_liquidity' ?
|
||||
<span>Up to 2 hours (delayed)</span>
|
||||
:
|
||||
<AverageCompletionTime time={destinationNetwork?.average_completion_time} />
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
export default DetailedEstimates
|
||||
@@ -0,0 +1,75 @@
|
||||
import { FC } from "react";
|
||||
import { Currency } from "../../../Models/Currency";
|
||||
import { Layer } from "../../../Models/Layer";
|
||||
import { GetDefaultNetwork, GetNetworkCurrency } from "../../../helpers/settingsHelper";
|
||||
import { CaluclateRefuelAmount } from "../../../lib/fees";
|
||||
import { truncateDecimals } from "../../utils/RoundDecimals";
|
||||
|
||||
type WillReceiveProps = {
|
||||
receive_amount?: number;
|
||||
currency?: Currency | null;
|
||||
to: Layer | undefined | null;
|
||||
currencies: Currency[];
|
||||
refuel: boolean
|
||||
}
|
||||
export const ReceiveAmounts: FC<WillReceiveProps> = ({ receive_amount, currency, to, currencies, refuel }) => {
|
||||
const parsedReceiveAmount = parseFloat(receive_amount?.toFixed(currency?.precision) || "")
|
||||
const destinationNetworkCurrency = (to && currency) ? GetNetworkCurrency(to, currency.asset) : null
|
||||
|
||||
return <>
|
||||
<span className="md:font-semibold text-sm md:text-base text-secondary-text leading-8 md:leading-8 flex-1">
|
||||
You will receive
|
||||
</span>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<span className="text-sm md:text-base">
|
||||
{
|
||||
parsedReceiveAmount > 0 ?
|
||||
<div className="font-semibold md:font-bold text-right leading-4">
|
||||
<p>
|
||||
<>{parsedReceiveAmount}</>
|
||||
|
||||
<span>
|
||||
{destinationNetworkCurrency?.name}
|
||||
</span>
|
||||
</p>
|
||||
{refuel && <Refuel
|
||||
currencies={currencies}
|
||||
currency={currency}
|
||||
to={to}
|
||||
refuel={refuel}
|
||||
/>}
|
||||
</div>
|
||||
: '-'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
type RefuelProps = {
|
||||
currency?: Currency | null;
|
||||
to?: Layer | null;
|
||||
currencies: Currency[];
|
||||
refuel: boolean
|
||||
}
|
||||
export const Refuel: FC<RefuelProps> = ({ to, currency, currencies, refuel }) => {
|
||||
const destination_native_asset = GetDefaultNetwork(to, currency?.asset)?.native_currency
|
||||
const refuel_native_currency = currencies.find(c => c.asset === destination_native_asset)
|
||||
const refuelCalculations = CaluclateRefuelAmount({
|
||||
allCurrencies: currencies,
|
||||
refuelEnabled: refuel,
|
||||
currency,
|
||||
to
|
||||
})
|
||||
const { refuelAmountInNativeCurrency } = refuelCalculations
|
||||
const truncated_refuel = truncateDecimals(refuelAmountInNativeCurrency, refuel_native_currency?.precision)
|
||||
|
||||
return <>
|
||||
{
|
||||
truncated_refuel > 0 ? <p className='text-[12px] text-secondary-text/50'>
|
||||
<>+</> <span>{truncated_refuel} {destination_native_asset}</span>
|
||||
</p>
|
||||
: null
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
import { useSettingsState } from '../../../context/settings';
|
||||
import { CalculateFee, CalculateReceiveAmount } from '../../../lib/fees';
|
||||
import { SwapFormValues } from '../../DTOs/SwapFormValues';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../../shadcn/accordion';
|
||||
import { ReceiveAmounts } from './ReceiveAmounts';
|
||||
import DetailedEstimates from './DetailedEstimates';
|
||||
import Campaign from './Campaign';
|
||||
|
||||
export default function FeeDetails({ values }: { values: SwapFormValues }) {
|
||||
const { networks, currencies } = useSettingsState()
|
||||
const { currency, from, to, refuel } = values || {}
|
||||
|
||||
let fee = CalculateFee(values, networks);
|
||||
let receive_amount = CalculateReceiveAmount(values, networks, currencies);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto relative w-full rounded-lg border border-secondary-500 hover:border-secondary-300 bg-secondary-700 px-3.5 py-3 z-[1] transition-all duration-200">
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value='item-1'>
|
||||
<AccordionTrigger className="items-center flex w-full relative gap-2 rounded-lg text-left text-base font-medium">
|
||||
<ReceiveAmounts
|
||||
currencies={currencies}
|
||||
currency={currency}
|
||||
to={to}
|
||||
receive_amount={receive_amount}
|
||||
refuel={!!refuel}
|
||||
/>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-sm text-secondary-text font-normal">
|
||||
<DetailedEstimates
|
||||
currencies={currencies}
|
||||
networks={networks}
|
||||
fee={fee}
|
||||
selected_currency={currency}
|
||||
source={from}
|
||||
destination={to}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
{
|
||||
values.to &&
|
||||
values.currency &&
|
||||
<Campaign
|
||||
destination={values.to}
|
||||
selected_currency={values.currency}
|
||||
fee={fee}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Home, RefreshCcw } from "lucide-react";
|
||||
import React from "react";
|
||||
import SubmitButton from "./buttons/submitButton"
|
||||
import ContactSupport from "./ContactSupport"
|
||||
import MessageComponent from "./MessageComponent"
|
||||
import Navbar from "./navbar"
|
||||
import GoHomeButton from "./utils/GoHome"
|
||||
|
||||
export default function ErrorFallback({ error, resetErrorBoundary }) {
|
||||
|
||||
const extension_error = error.stack.includes("chrome-extension", "app://")
|
||||
|
||||
return (
|
||||
<div className="styled-scroll">
|
||||
<main className="styled-scroll">
|
||||
<div className="min-h-screen overflow-hidden relative font-robo">
|
||||
<Navbar />
|
||||
<div className="mx-auto max-w-xl bg-secondary-900 shadow-card rounded-lg w-full overflow-hidden relative px-0 md:px-8 py-6 h-[500px] min-h-[550px]">
|
||||
<MessageComponent>
|
||||
<MessageComponent.Content icon="red">
|
||||
<MessageComponent.Header>
|
||||
Unable to complete the request
|
||||
</MessageComponent.Header>
|
||||
<MessageComponent.Description>
|
||||
<p>
|
||||
<span>Sorry, but we were unable to complete this request. </span>
|
||||
{
|
||||
extension_error ?
|
||||
<span>It seems that some of your extensions are preventing the app from running.</span>
|
||||
: <span>We are informed, and are now investigating the issue.</span>
|
||||
}
|
||||
|
||||
</p>
|
||||
<p>
|
||||
{
|
||||
extension_error ?
|
||||
<span>Please disable extensions and try again or open in incognito mode. If the issue keeps happening, </span>
|
||||
:
|
||||
<span>Please try again. If the issue keeps happening, </span>
|
||||
}
|
||||
|
||||
<span className="underline cursor-pointer text-primary "><ContactSupport>contact our support team.</ContactSupport></span>
|
||||
</p>
|
||||
</MessageComponent.Description>
|
||||
</MessageComponent.Content>
|
||||
<MessageComponent.Buttons>
|
||||
<div className="flex flex-row text-primary-text text-xs sm:text-base space-x-2">
|
||||
<div className='basis-1/3'>
|
||||
{
|
||||
<GoHomeButton>
|
||||
<SubmitButton text_align="left" buttonStyle="outline" isDisabled={false} isSubmitting={false} icon={<Home className="h-5 w-5" aria-hidden="true" />}>
|
||||
Go home
|
||||
</SubmitButton>
|
||||
</GoHomeButton>
|
||||
}
|
||||
</div>
|
||||
<div className='basis-2/3'>
|
||||
<SubmitButton button_align="right" text_align="left" isDisabled={false} isSubmitting={false}
|
||||
onClick={() =>
|
||||
resetErrorBoundary()
|
||||
}
|
||||
icon={<RefreshCcw className="h-5 w-5" aria-hidden="true" />}>
|
||||
Try Again
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</MessageComponent.Buttons>
|
||||
</MessageComponent>
|
||||
</div>
|
||||
<div id="widget_root" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useIntercom } from "react-use-intercom"
|
||||
import { useAuthState } from "../../context/authContext"
|
||||
import IconButton from "../buttons/iconButton"
|
||||
import GoHomeButton from "../utils/GoHome"
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import ChatIcon from "../icons/ChatIcon"
|
||||
import dynamic from "next/dynamic"
|
||||
import BridgeMenu from "../BridgeMenu"
|
||||
|
||||
const WalletsHeader = dynamic(() => import("../ConnectedWallets").then((comp) => comp.WalletsHeader), {
|
||||
loading: () => <></>
|
||||
})
|
||||
|
||||
function HeaderWithMenu({ goBack }: { goBack: (() => void) | undefined | null }) {
|
||||
const { email, userId } = useAuthState()
|
||||
const { boot, show, update } = useIntercom()
|
||||
const updateWithProps = () => update({ email: email, userId: userId })
|
||||
|
||||
return (
|
||||
<div className="w-full grid grid-cols-5 px-6 mt-3" >
|
||||
{
|
||||
goBack &&
|
||||
<IconButton onClick={goBack}
|
||||
aria-label="Go back"
|
||||
icon={
|
||||
<ArrowLeft strokeWidth="3" />
|
||||
}>
|
||||
</IconButton>
|
||||
}
|
||||
<div className='justify-self-center self-center col-start-2 col-span-3 mx-auto overflow-hidden md:hidden'>
|
||||
<GoHomeButton />
|
||||
</div>
|
||||
|
||||
<div className="col-start-5 justify-self-end self-center flex items-center gap-4">
|
||||
<WalletsHeader />
|
||||
<IconButton className="relative hidden md:inline" onClick={() => {
|
||||
boot();
|
||||
show();
|
||||
updateWithProps()
|
||||
}}
|
||||
icon={
|
||||
<ChatIcon className="h-6 w-6" strokeWidth="2" />
|
||||
}>
|
||||
</IconButton>
|
||||
<div className="fixed-width-container">
|
||||
<BridgeMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderWithMenu
|
||||
@@ -0,0 +1,342 @@
|
||||
import { useFormikContext } from "formik";
|
||||
import { ChangeEvent, FC, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AddressBookItem } from "../../lib/BridgeApiClient";
|
||||
import { SwapFormValues } from "../DTOs/SwapFormValues";
|
||||
import { classNames } from '../utils/classNames'
|
||||
import { useSwapDataUpdate } from "../../context/swap";
|
||||
import { Info } from "lucide-react";
|
||||
import KnownInternalNames from "../../lib/knownIds";
|
||||
import { useSettingsState } from "../../context/settings";
|
||||
import { isValidAddress } from "../../lib/addressValidator";
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import Image from 'next/image';
|
||||
import { Partner } from "../../Models/Partner";
|
||||
import shortenAddress from "../utils/ShortenAddress";
|
||||
import AddressIcon from "../AddressIcon";
|
||||
import { GetDefaultNetwork } from "../../helpers/settingsHelper";
|
||||
import WalletIcon from "../icons/WalletIcon";
|
||||
import useWallet from "../../hooks/useWallet";
|
||||
|
||||
interface Input extends Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'as' | 'onChange'> {
|
||||
hideLabel?: boolean;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
ref?: any;
|
||||
close: () => void,
|
||||
isPartnerWallet: boolean,
|
||||
partnerImage?: string,
|
||||
partner?: Partner,
|
||||
canFocus?: boolean,
|
||||
address_book?: AddressBookItem[]
|
||||
}
|
||||
|
||||
const Address: FC<Input> = forwardRef<HTMLInputElement, Input>(function Address
|
||||
({ name, canFocus, close, address_book, disabled, isPartnerWallet, partnerImage, partner }, ref) {
|
||||
const {
|
||||
values,
|
||||
setFieldValue
|
||||
} = useFormikContext<SwapFormValues>();
|
||||
|
||||
const [wrongNetwork, setWrongNetwork] = useState(false)
|
||||
const inputReference = useRef<HTMLInputElement>(null);
|
||||
const destination = values.to
|
||||
const asset = values.currency?.asset
|
||||
const destinationNetwork = GetDefaultNetwork(destination, asset)
|
||||
const valid_addresses = address_book?.filter(a => (destination?.isExchange ? a.exchanges?.some(e => destination?.internal_name === e) : a.networks?.some(n => destination?.internal_name === n)) && isValidAddress(a.address, destination)) || []
|
||||
|
||||
const { setDepositeAddressIsfromAccount, setAddressConfirmed } = useSwapDataUpdate()
|
||||
const placeholder = "Enter your address here"
|
||||
const [inputValue, setInputValue] = useState<string | undefined>(values?.destination_address || "")
|
||||
const [validInputAddress, setValidInputAddress] = useState<string | undefined>('')
|
||||
const destinationIsStarknet = destination?.internal_name === KnownInternalNames.Networks.StarkNetGoerli
|
||||
|| destination?.internal_name === KnownInternalNames.Networks.StarkNetMainnet
|
||||
|
||||
const { connectWallet, disconnectWallet, getAutofillProvider: getProvider } = useWallet()
|
||||
const provider = useMemo(() => {
|
||||
return values?.to && getProvider(values?.to)
|
||||
}, [values?.to, getProvider])
|
||||
|
||||
const connectedWallet = provider?.getConnectedWallet()
|
||||
const settings = useSettingsState()
|
||||
|
||||
useEffect(() => {
|
||||
if (destination && !destination?.isExchange && isValidAddress(connectedWallet?.address, destination) && !values?.destination_address) {
|
||||
//TODO move to wallet implementation
|
||||
if (connectedWallet
|
||||
&& connectedWallet.providerName === 'starknet'
|
||||
&& (connectedWallet.chainId != destinationChainId)
|
||||
&& destination) {
|
||||
(async () => {
|
||||
setWrongNetwork(true)
|
||||
await disconnectWallet(connectedWallet.providerName)
|
||||
})()
|
||||
return
|
||||
}
|
||||
setInputValue(connectedWallet?.address)
|
||||
setAddressConfirmed(true)
|
||||
setFieldValue("destination_address", connectedWallet?.address)
|
||||
}
|
||||
}, [connectedWallet?.address, destination?.isExchange, destination])
|
||||
|
||||
useEffect(() => {
|
||||
if (canFocus) {
|
||||
inputReference?.current?.focus()
|
||||
}
|
||||
}, [canFocus])
|
||||
|
||||
useEffect(() => {
|
||||
values.destination_address && setInputValue(values.destination_address)
|
||||
}, [values.destination_address])
|
||||
|
||||
const handleRemoveDepositeAddress = useCallback(async () => {
|
||||
setDepositeAddressIsfromAccount(false)
|
||||
setFieldValue("destination_address", '')
|
||||
setInputValue("")
|
||||
}, [setDepositeAddressIsfromAccount, setFieldValue, values.to])
|
||||
|
||||
const handleSelectAddress = useCallback((value: string) => {
|
||||
setAddressConfirmed(true)
|
||||
setFieldValue("destination_address", value)
|
||||
close()
|
||||
}, [close, setAddressConfirmed, setFieldValue])
|
||||
|
||||
const inputAddressIsValid = isValidAddress(inputValue, destination)
|
||||
let errorMessage = '';
|
||||
if (inputValue && !isValidAddress(inputValue, destination)) {
|
||||
errorMessage = `Enter a valid ${values.to?.display_name} address`
|
||||
}
|
||||
|
||||
const handleInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
setAddressConfirmed(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (inputAddressIsValid) {
|
||||
setValidInputAddress(inputValue)
|
||||
}
|
||||
}, [inputValue, inputAddressIsValid])
|
||||
|
||||
const handleSetNewAddress = useCallback(() => {
|
||||
setAddressConfirmed(true)
|
||||
setFieldValue("destination_address", validInputAddress)
|
||||
close()
|
||||
}, [validInputAddress])
|
||||
|
||||
const destinationAsset = destination?.assets?.find(a => a.asset === asset)
|
||||
const destinationChainId = destinationAsset?.network?.chain_id
|
||||
|
||||
return (<>
|
||||
<div className='w-full flex flex-col justify-between h-full text-primary-text'>
|
||||
<div className='flex flex-col self-center grow w-full'>
|
||||
<div className={`flex flex-col self-center grow w-full space-y-3`}>
|
||||
<div className="text-left">
|
||||
<label htmlFor={name}>Address</label>
|
||||
{isPartnerWallet && partner && <span className='truncate text-sm text-indigo-200'> ({partner?.display_name})</span>}
|
||||
<div className="flex flex-wrap flex-col md:flex-row">
|
||||
<div className="relative flex grow rounded-lg shadow-sm mt-1.5 bg-secondary-700 border-secondary-500 border focus-within:ring-0 focus-within:ring-primary focus-within:border-primary">
|
||||
{isPartnerWallet &&
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
{
|
||||
partnerImage &&
|
||||
<Image alt="Partner logo" className='rounded-md object-contain' src={partnerImage} width="24" height="24"></Image>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<input
|
||||
onChange={handleInputChange}
|
||||
value={inputValue}
|
||||
placeholder={placeholder}
|
||||
autoCorrect="off"
|
||||
type={"text"}
|
||||
disabled={disabled || !!(connectedWallet && values.destination_address)}
|
||||
name={name}
|
||||
id={name}
|
||||
ref={inputReference}
|
||||
tabIndex={0}
|
||||
className={`${isPartnerWallet ? 'pl-11' : ''} disabled:cursor-not-allowed grow h-12 border-none leading-4 block font-semibold w-full bg-secondary-700 rounded-lg truncate hover:overflow-x-scroll focus:ring-0 focus:outline-none`}
|
||||
/>
|
||||
{
|
||||
inputValue && !disabled &&
|
||||
<span className="inline-flex items-center mr-2">
|
||||
<div className="text-xs flex items-center space-x-2 md:ml-5 bg-secondary-500 rounded-md border border-secondary-500">
|
||||
<button
|
||||
type="button"
|
||||
className="p-0.5 duration-200 transition hover:bg-secondary-400 rounded-md border border-secondary-500 hover:border-secondary-200"
|
||||
onClick={handleRemoveDepositeAddress}
|
||||
>
|
||||
<div className="flex items-center px-2 text-sm py-1 font-semibold">
|
||||
Clear
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
{errorMessage &&
|
||||
<div className="basis-full text-xs text-primary">
|
||||
{errorMessage}
|
||||
</div>
|
||||
}
|
||||
{wrongNetwork && !inputValue &&
|
||||
<div className="basis-full text-xs text-primary">
|
||||
{
|
||||
destination?.internal_name === KnownInternalNames.Networks.StarkNetMainnet
|
||||
? <span>Please switch to Starknet Mainnet with your wallet and click Autofill again</span>
|
||||
: <span>Please switch to Starknet Goerli with your wallet and click Autofill again</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
validInputAddress &&
|
||||
<div onClick={handleSetNewAddress} className={`text-left min-h-12 cursor-pointer space-x-2 border border-secondary-300 bg-secondary-600 shadow-xl flex text-sm rounded-md items-center w-full transform hover:bg-secondary-500 transition duration-200 px-2 py-2 hover:border-secondary-500 hover:shadow-xl`}>
|
||||
<div className='flex text-primary-text bg-secondary-400 flex-row items-left rounded-md p-2'>
|
||||
{
|
||||
destinationIsStarknet && connectedWallet ?
|
||||
<connectedWallet.icon className='rounded-md' alt={connectedWallet?.address} width={25} height={25} />
|
||||
:
|
||||
<AddressIcon address={validInputAddress} size={25} />
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col grow">
|
||||
<div className="block text-md font-medium text-primary-text">
|
||||
{shortenAddress(validInputAddress)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex text-primary-text flex-row items-left px-2 py-1 rounded-md'>
|
||||
Select
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!disabled
|
||||
&& !inputValue
|
||||
&& destination
|
||||
&& !destination?.isExchange
|
||||
&& destinationNetwork
|
||||
&& provider
|
||||
&& !connectedWallet &&
|
||||
<div onClick={() => { connectWallet(provider.name) }} className={`min-h-12 text-left cursor-pointer space-x-2 border border-secondary-500 bg-secondary-700/70 flex text-sm rounded-md items-center w-full transform transition duration-200 px-2 py-1.5 hover:border-secondary-500 hover:bg-secondary-700 hover:shadow-xl`}>
|
||||
<div className='flex text-primary-text flex-row items-left bg-secondary-400 px-2 py-1 rounded-md'>
|
||||
<WalletIcon className="w-6 h-6 text-primary-text" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="block text-sm font-medium">
|
||||
Autofill from wallet
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
Connect your wallet to fetch the address
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
destination?.isExchange
|
||||
&& !inputAddressIsValid
|
||||
&& values.currency
|
||||
&& destinationNetwork
|
||||
&&
|
||||
<div className='text-left p-4 bg-secondary-800 text-primary-text rounded-lg border border-secondary-500'>
|
||||
<div className="flex items-center">
|
||||
<Info className='h-5 w-5 text-primary-600 mr-3' />
|
||||
<label className="block text-sm md:text-base font-medium leading-6">How to find your {destination?.display_name} deposit address</label>
|
||||
</div>
|
||||
<ul className="list-disc font-light space-y-1 text-xs md:text-sm mt-2 ml-8 text-primary-text">
|
||||
<li>Go to the Deposits page</li>
|
||||
<li>
|
||||
<span>Select</span>
|
||||
<span className="inline-block mx-1">
|
||||
<span className='flex gap-1 items-baseline text-sm '>
|
||||
<Image src={settings.resolveImgSrc(values.currency)}
|
||||
alt="Project Logo"
|
||||
height="15"
|
||||
width="15"
|
||||
className='rounded-sm'
|
||||
/>
|
||||
<span className="text-primary-text">{values.currency.asset}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span>as asset</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Select</span>
|
||||
<span className="inline-block mx-1">
|
||||
<span className='flex gap-1 items-baseline text-sm '>
|
||||
<Image src={settings.resolveImgSrc(destinationNetwork)}
|
||||
alt="Project Logo"
|
||||
height="15"
|
||||
width="15"
|
||||
className='rounded-sm'
|
||||
/>
|
||||
<span className="text-primary-text">{destinationNetwork?.display_name}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span>as network</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!disabled && valid_addresses?.length > 0 && !inputValue &&
|
||||
<div className="text-left">
|
||||
<label className="">Recently used</label>
|
||||
<RadioGroup disabled={disabled} value={values.destination_address} onChange={handleSelectAddress}>
|
||||
<div className="rounded-md overflow-y-auto styled-scroll max-h-[300px]">
|
||||
{valid_addresses?.map((a) => (
|
||||
<RadioGroup.Option
|
||||
key={a.address}
|
||||
value={a.address}
|
||||
disabled={disabled}
|
||||
className={({ disabled }) =>
|
||||
classNames(
|
||||
disabled ? ' cursor-not-allowed ' : ' cursor-pointer ',
|
||||
'relative flex focus:outline-none mt-2 mb-3 '
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ checked }) => {
|
||||
const difference_in_days = Math.round(Math.abs(((new Date()).getTime() - new Date(a.date).getTime()) / (1000 * 3600 * 24)))
|
||||
return (
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className={`space-x-2 flex text-sm rounded-md items-center w-full transform transition duration-200 px-2 py-1.5 border border-secondary-900 hover:border-secondary-500 hover:bg-secondary-700/70 hover:shadow-xl ${checked && 'border-secondary-700'}`}
|
||||
>
|
||||
<div className='flex bg-secondary-400 text-primary-text flex-row items-left rounded-md p-2'>
|
||||
<AddressIcon address={a.address} size={20} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="block text-sm font-medium">
|
||||
{shortenAddress(a.address)}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
{
|
||||
difference_in_days === 0 ?
|
||||
<>Used today</>
|
||||
:
|
||||
(difference_in_days > 1 ?
|
||||
<>Used {difference_in_days} days ago</>
|
||||
: <>Used yesterday</>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup.Description>
|
||||
)
|
||||
}}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
export default Address
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useFormikContext } from "formik";
|
||||
import { forwardRef } from "react";
|
||||
import { SwapFormValues } from "../DTOs/SwapFormValues";
|
||||
import NumericInput from "./NumericInput";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const EnhancedAmountField = dynamic(() => import("./EnhancedAmount"), {
|
||||
loading: () => <NumericInput
|
||||
label={<div className="flex items-center w-full justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Amount</p>
|
||||
</div>
|
||||
</div>}
|
||||
disabled={true}
|
||||
placeholder='0.01234'
|
||||
name="amount"
|
||||
className="rounded-r-none text-primary-text"
|
||||
>
|
||||
</NumericInput>
|
||||
});
|
||||
|
||||
const AmountField = forwardRef(function AmountField(_, ref: any) {
|
||||
|
||||
const { values } = useFormikContext<SwapFormValues>();
|
||||
const { from, to } = values
|
||||
const name = "amount"
|
||||
|
||||
if (!from || !to)
|
||||
return <NumericInput
|
||||
label={<div className="flex items-center w-full justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Amount</p>
|
||||
</div>
|
||||
</div>}
|
||||
disabled={true}
|
||||
placeholder='0.01234'
|
||||
name={name}
|
||||
className="rounded-r-none text-primary-text"
|
||||
>
|
||||
</NumericInput>
|
||||
|
||||
return <EnhancedAmountField />
|
||||
});
|
||||
|
||||
|
||||
export default AmountField
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useFormikContext } from "formik";
|
||||
import { FC, useCallback, useEffect, useMemo } from "react";
|
||||
import { useSettingsState } from "../../context/settings";
|
||||
import { SwapFormValues } from "../DTOs/SwapFormValues";
|
||||
import { FilterCurrencies, GetNetworkCurrency } from "../../helpers/settingsHelper";
|
||||
import { Currency } from "../../Models/Currency";
|
||||
import { SelectMenuItem } from "../Select/Shared/Props/selectMenuItem";
|
||||
import PopoverSelectWrapper from "../Select/Popover/PopoverSelectWrapper";
|
||||
import CurrencySettings from "../../lib/CurrencySettings";
|
||||
import { SortingByOrder } from "../../lib/sorting";
|
||||
import { Layer } from "../../Models/Layer";
|
||||
import { useBalancesState } from "../../context/balances";
|
||||
import { truncateDecimals } from "../utils/RoundDecimals";
|
||||
import { useQueryState } from "../../context/query";
|
||||
import useWallet from "../../hooks/useWallet";
|
||||
import { Balance } from "../../Models/Balance";
|
||||
|
||||
const CurrencyFormField: FC = () => {
|
||||
const {
|
||||
values: { to, currency, from },
|
||||
setFieldValue,
|
||||
} = useFormikContext<SwapFormValues>();
|
||||
|
||||
const { resolveImgSrc, currencies } = useSettingsState();
|
||||
const name = "currency"
|
||||
const query = useQueryState()
|
||||
const { balances } = useBalancesState()
|
||||
const { getAutofillProvider: getProvider } = useWallet()
|
||||
const provider = useMemo(() => {
|
||||
return from && getProvider(from)
|
||||
}, [from, getProvider])
|
||||
|
||||
const wallet = provider?.getConnectedWallet()
|
||||
const lockedCurrency = query?.lockAsset ? currencies?.find(c => c?.asset?.toUpperCase() === (query?.asset as string)?.toUpperCase()) : undefined
|
||||
|
||||
const filteredCurrencies = lockedCurrency ? [lockedCurrency] : FilterCurrencies(currencies, from, to)
|
||||
const currencyMenuItems = from ? GenerateCurrencyMenuItems(
|
||||
filteredCurrencies,
|
||||
from,
|
||||
resolveImgSrc,
|
||||
lockedCurrency,
|
||||
balances[wallet?.address || '']
|
||||
) : []
|
||||
|
||||
useEffect(() => {
|
||||
const currencyIsAvailable = currency && currencyMenuItems.some(c => c?.baseObject.asset === currency?.asset)
|
||||
if (currencyIsAvailable) return
|
||||
|
||||
const default_currency = currencyMenuItems.find(c => c.baseObject?.asset?.toUpperCase() === (query?.asset as string)?.toUpperCase()) || currencyMenuItems?.[0]
|
||||
|
||||
if (default_currency) {
|
||||
setFieldValue(name, default_currency.baseObject)
|
||||
}
|
||||
else if (currency) {
|
||||
setFieldValue(name, null)
|
||||
}
|
||||
}, [from, to, currencies, currency, query])
|
||||
|
||||
const value = currencyMenuItems.find(x => x.id == currency?.asset);
|
||||
const handleSelect = useCallback((item: SelectMenuItem<Currency>) => {
|
||||
setFieldValue(name, item.baseObject, true)
|
||||
}, [name])
|
||||
|
||||
return <PopoverSelectWrapper values={currencyMenuItems} value={value} setValue={handleSelect} disabled={!value?.isAvailable?.value} />;
|
||||
};
|
||||
|
||||
export function GenerateCurrencyMenuItems(currencies: Currency[], source: Layer, resolveImgSrc: (item: Layer | Currency) => string, lockedCurrency?: Currency, balances?: Balance[]): SelectMenuItem<Currency>[] {
|
||||
|
||||
let currencyIsAvailable = () => {
|
||||
if (lockedCurrency) {
|
||||
return { value: false, disabledReason: CurrencyDisabledReason.LockAssetIsTrue }
|
||||
}
|
||||
else {
|
||||
return { value: true, disabledReason: null }
|
||||
}
|
||||
}
|
||||
|
||||
return currencies.map(c => {
|
||||
const sourceCurrency = GetNetworkCurrency(source, c.asset);
|
||||
const displayName = lockedCurrency?.asset ?? (source?.isExchange ? sourceCurrency?.asset : sourceCurrency?.name);
|
||||
const balance = balances?.find(b => b?.token === c?.asset && source.internal_name === b.network)
|
||||
const formatted_balance_amount = balance ? Number(truncateDecimals(balance?.amount, c.precision)) : ''
|
||||
|
||||
const res: SelectMenuItem<Currency> = {
|
||||
baseObject: c,
|
||||
id: c.asset,
|
||||
name: displayName || "-",
|
||||
order: CurrencySettings.KnownSettings[c.asset]?.Order ?? 5,
|
||||
imgSrc: resolveImgSrc && resolveImgSrc(c),
|
||||
isAvailable: currencyIsAvailable(),
|
||||
details: `${formatted_balance_amount}`
|
||||
};
|
||||
return res
|
||||
}).sort(SortingByOrder);
|
||||
}
|
||||
|
||||
export enum CurrencyDisabledReason {
|
||||
LockAssetIsTrue = '',
|
||||
InsufficientLiquidity = 'Temporarily disabled. Please check later.'
|
||||
}
|
||||
|
||||
export default CurrencyFormField
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useFormikContext } from "formik";
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useSettingsState } from "../../context/settings";
|
||||
import { CalculateMaxAllowedAmount, CalculateMinAllowedAmount } from "../../lib/fees";
|
||||
import { SwapFormValues } from "../DTOs/SwapFormValues";
|
||||
import CurrencyFormField from "./CurrencyFormField";
|
||||
import NumericInput from "./NumericInput";
|
||||
import SecondaryButton from "../buttons/secondaryButton";
|
||||
import { useQueryState } from "../../context/query";
|
||||
import { useBalancesState } from "../../context/balances";
|
||||
import { truncateDecimals } from "../utils/RoundDecimals";
|
||||
import useWallet from "../../hooks/useWallet";
|
||||
import useBalance from "../../hooks/useBalance";
|
||||
|
||||
const EnhancedAmountField = forwardRef(function EnhancedAmountField(_, ref: any) {
|
||||
|
||||
const { values, setFieldValue } = useFormikContext<SwapFormValues>();
|
||||
const { networks, currencies } = useSettingsState()
|
||||
const query = useQueryState()
|
||||
const { currency, from, to, amount, destination_address } = values
|
||||
|
||||
const { balances, isBalanceLoading, gases, isGasLoading } = useBalancesState()
|
||||
const { getAutofillProvider: getProvider } = useWallet()
|
||||
const provider = useMemo(() => {
|
||||
return values.from && getProvider(values.from)
|
||||
}, [values.from, getProvider])
|
||||
|
||||
const wallet = provider?.getConnectedWallet()
|
||||
const gasAmount = gases[from?.internal_name || '']?.find(g => g?.token === currency?.asset)?.gas || 0
|
||||
const { fetchBalance, fetchGas } = useBalance()
|
||||
const name = "amount"
|
||||
const walletBalance = wallet && balances[wallet.address]?.find(b => b?.network === from?.internal_name && b?.token === currency?.asset)
|
||||
const walletBalanceAmount = walletBalance?.amount && truncateDecimals(walletBalance?.amount, currency?.precision)
|
||||
|
||||
const minAllowedAmount = CalculateMinAllowedAmount(values, networks, currencies);
|
||||
const maxAllowedAmount = CalculateMaxAllowedAmount(values, query.balances as string, walletBalance?.amount, gasAmount, minAllowedAmount)
|
||||
const maxAllowedDisplayAmont = truncateDecimals(maxAllowedAmount, currency?.precision)
|
||||
|
||||
const placeholder = (currency && from && to && !isBalanceLoading && !isGasLoading) ? `${minAllowedAmount} - ${maxAllowedDisplayAmont}` : '0.01234'
|
||||
const step = 1 / Math.pow(10, currency?.precision || 1)
|
||||
const amountRef = useRef(ref)
|
||||
|
||||
const handleSetMinAmount = () => {
|
||||
setFieldValue(name, minAllowedAmount)
|
||||
}
|
||||
|
||||
const handleSetMaxAmount = useCallback(() => {
|
||||
setFieldValue(name, maxAllowedAmount);
|
||||
from && fetchBalance(from);
|
||||
from && currency && fetchGas(from, currency, destination_address || "");
|
||||
}, [from, currency, destination_address, maxAllowedAmount])
|
||||
|
||||
useEffect(() => {
|
||||
values.from && fetchBalance(values.from)
|
||||
}, [values.from, values.destination_address, wallet?.address])
|
||||
|
||||
const contract_address = values.from?.isExchange == false ? values.from.assets.find(a => a.asset === values?.currency?.asset)?.contract_address : null
|
||||
useEffect(() => {
|
||||
wallet?.address && values.from && values.currency && fetchGas(values.from, values.currency, values.destination_address || wallet.address)
|
||||
}, [contract_address, values.from, values.currency, wallet?.address])
|
||||
|
||||
return (<>
|
||||
<NumericInput
|
||||
label={<AmountLabel detailsAvailable={!!(from && to && amount)}
|
||||
maxAllowedAmount={maxAllowedDisplayAmont}
|
||||
minAllowedAmount={minAllowedAmount}
|
||||
isBalanceLoading={(isBalanceLoading || isGasLoading)}
|
||||
walletBalance={walletBalanceAmount}
|
||||
/>}
|
||||
disabled={!currency}
|
||||
placeholder={placeholder}
|
||||
min={minAllowedAmount}
|
||||
max={maxAllowedAmount}
|
||||
step={isNaN(step) ? 0.01 : step}
|
||||
name={name}
|
||||
ref={amountRef}
|
||||
precision={currency?.precision}
|
||||
className="rounded-r-none text-primary-text"
|
||||
>
|
||||
{
|
||||
from && to && currency ? <div className="text-xs flex items-center space-x-1 md:space-x-2 ml-2 md:ml-5">
|
||||
<SecondaryButton onClick={handleSetMinAmount} size="xs">
|
||||
MIN
|
||||
</SecondaryButton>
|
||||
<SecondaryButton onClick={handleSetMaxAmount} size="xs">
|
||||
MAX
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
<CurrencyFormField />
|
||||
</NumericInput>
|
||||
</>)
|
||||
});
|
||||
|
||||
|
||||
|
||||
type AmountLabelProps = {
|
||||
detailsAvailable: boolean;
|
||||
minAllowedAmount: number;
|
||||
maxAllowedAmount: number;
|
||||
isBalanceLoading: boolean;
|
||||
walletBalance?: number
|
||||
}
|
||||
const AmountLabel = ({
|
||||
detailsAvailable,
|
||||
minAllowedAmount,
|
||||
maxAllowedAmount,
|
||||
isBalanceLoading,
|
||||
walletBalance,
|
||||
}: AmountLabelProps) => {
|
||||
return <div className="flex items-center w-full justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Amount</p>
|
||||
{
|
||||
detailsAvailable &&
|
||||
<div className="text-xs hidden md:flex text-secondary-text items-center">
|
||||
<span>(Min: </span>{minAllowedAmount}<span> - Max: </span>{isBalanceLoading ? <span className="ml-1 h-3 w-6 rounded-sm bg-gray-500 animate-pulse" /> : <span>{maxAllowedAmount}</span>}<span>)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
walletBalance != undefined && !isNaN(walletBalance) &&
|
||||
<div
|
||||
className="border-primary-text text-xs text-primary-text flex items-center space-x-1">
|
||||
<span>Balance:</span>
|
||||
{isBalanceLoading ?
|
||||
<span className="ml-1 h-3 w-6 rounded-sm bg-gray-500 animate-pulse" />
|
||||
:
|
||||
<span>{walletBalance}</span>}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default EnhancedAmountField
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useFormikContext } from "formik";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
import { useSettingsState } from "../../context/settings";
|
||||
import { SwapFormValues } from "../DTOs/SwapFormValues";
|
||||
import { ISelectMenuItem, SelectMenuItem } from "../Select/Shared/Props/selectMenuItem";
|
||||
import { Layer } from "../../Models/Layer";
|
||||
import CommandSelectWrapper from "../Select/Command/CommandSelectWrapper";
|
||||
import { FilterDestinationLayers, FilterSourceLayers } from "../../helpers/settingsHelper";
|
||||
import { Currency } from "../../Models/Currency";
|
||||
import ExchangeSettings from "../../lib/ExchangeSettings";
|
||||
import { SortingByOrder } from "../../lib/sorting"
|
||||
import { LayerDisabledReason } from "../Select/Popover/PopoverSelect";
|
||||
import NetworkSettings from "../../lib/NetworkSettings";
|
||||
import { SelectMenuItemGroup } from "../Select/Command/commandSelect";
|
||||
import { useQueryState } from "../../context/query";
|
||||
|
||||
type SwapDirection = "from" | "to";
|
||||
type Props = {
|
||||
direction: SwapDirection,
|
||||
label: string,
|
||||
}
|
||||
const GROUP_ORDERS = { "Popular": 1, "New": 2, "Fiat": 3, "Networks": 4, "Exchanges": 5, "Other": 10 };
|
||||
const getGroupName = (layer: Layer) => {
|
||||
|
||||
if (layer.is_featured) {
|
||||
return "Popular";
|
||||
}
|
||||
else if (new Date(layer.created_date).getTime() >= (new Date().getTime() - 2629800000)) {
|
||||
return "New";
|
||||
}
|
||||
else if (!layer.isExchange) {
|
||||
return "Networks";
|
||||
}
|
||||
else if (layer.type === 'fiat') {
|
||||
return "Fiat";
|
||||
}
|
||||
else if (layer.type === 'cex') {
|
||||
return "Exchanges";
|
||||
}
|
||||
else {
|
||||
return "Other";
|
||||
}
|
||||
}
|
||||
|
||||
const NetworkFormField = forwardRef(function NetworkFormField({ direction, label }: Props, ref: any) {
|
||||
const {
|
||||
values,
|
||||
setFieldValue,
|
||||
} = useFormikContext<SwapFormValues>();
|
||||
const name = direction
|
||||
const { from, to } = values
|
||||
const { lockFrom, lockTo, asset, lockAsset } = useQueryState()
|
||||
const { resolveImgSrc, layers, currencies } = useSettingsState();
|
||||
|
||||
let placeholder = "";
|
||||
let searchHint = "";
|
||||
let filteredLayers: Layer[];
|
||||
let menuItems: SelectMenuItem<Layer>[];
|
||||
const lockedCurrency = lockAsset ?
|
||||
currencies?.find(c => c?.asset?.toUpperCase() === (asset as string)?.toUpperCase())
|
||||
: null
|
||||
|
||||
let valueGrouper: (values: ISelectMenuItem[]) => SelectMenuItemGroup[];
|
||||
|
||||
if (direction === "from") {
|
||||
placeholder = "Source";
|
||||
searchHint = "Swap from";
|
||||
filteredLayers = FilterSourceLayers(layers, to, lockedCurrency);
|
||||
menuItems = GenerateMenuItems(filteredLayers, resolveImgSrc, direction, !!(from && lockFrom));
|
||||
}
|
||||
else {
|
||||
placeholder = "Destination";
|
||||
searchHint = "Swap to";
|
||||
filteredLayers = FilterDestinationLayers(layers, from, lockedCurrency);
|
||||
menuItems = GenerateMenuItems(filteredLayers, resolveImgSrc, direction, !!(to && lockTo));
|
||||
}
|
||||
valueGrouper = groupByType
|
||||
|
||||
const value = menuItems.find(x => x.id == (direction === "from" ? from : to)?.internal_name);
|
||||
const handleSelect = useCallback((item: SelectMenuItem<Layer>) => {
|
||||
setFieldValue(name, item.baseObject, true)
|
||||
}, [name])
|
||||
|
||||
return (<div className="rounded-xl p-3 bg-secondary-700">
|
||||
<label htmlFor={name} className="block font-semibold text-secondary-text text-sm">
|
||||
{label}
|
||||
</label>
|
||||
<div ref={ref} className={`mt-1.5 `}>
|
||||
<CommandSelectWrapper
|
||||
disabled={false}
|
||||
valueGrouper={valueGrouper}
|
||||
placeholder={placeholder}
|
||||
setValue={handleSelect}
|
||||
value={value}
|
||||
values={menuItems}
|
||||
searchHint={searchHint}
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
});
|
||||
|
||||
function groupByType(values: ISelectMenuItem[]) {
|
||||
let groups: SelectMenuItemGroup[] = [];
|
||||
values.forEach((v) => {
|
||||
let group = groups.find(x => x.name == v.group) || new SelectMenuItemGroup({ name: v.group, items: [] });
|
||||
group.items.push(v);
|
||||
if (!groups.find(x => x.name == v.group)) {
|
||||
groups.push(group);
|
||||
}
|
||||
});
|
||||
|
||||
groups.forEach(group => {
|
||||
group.items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
groups.sort((a, b) => {
|
||||
// Sort put networks first then exchanges
|
||||
return (GROUP_ORDERS[a.name] || GROUP_ORDERS.Other) - (GROUP_ORDERS[b.name] || GROUP_ORDERS.Other);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function GenerateMenuItems(layers: Layer[], resolveImgSrc: (item: Layer | Currency) => string, direction: SwapDirection, lock: boolean): SelectMenuItem<Layer>[] {
|
||||
|
||||
let layerIsAvailable = (layer: Layer) => {
|
||||
if (lock) {
|
||||
return { value: false, disabledReason: LayerDisabledReason.LockNetworkIsTrue }
|
||||
}
|
||||
else {
|
||||
return { value: true, disabledReason: null }
|
||||
}
|
||||
}
|
||||
|
||||
return layers.map(l => {
|
||||
let orderProp: keyof NetworkSettings | keyof ExchangeSettings = direction == 'from' ? 'OrderInSource' : 'OrderInDestination';
|
||||
const order = (l.isExchange ?
|
||||
ExchangeSettings.KnownSettings[l.internal_name]?.[orderProp]
|
||||
: NetworkSettings.KnownSettings[l.internal_name]?.[orderProp])
|
||||
const res: SelectMenuItem<Layer> = {
|
||||
baseObject: l,
|
||||
id: l.internal_name,
|
||||
name: l.display_name,
|
||||
order: order || 100,
|
||||
imgSrc: resolveImgSrc && resolveImgSrc(l),
|
||||
isAvailable: layerIsAvailable(l),
|
||||
group: getGroupName(l)
|
||||
}
|
||||
return res;
|
||||
}).sort(SortingByOrder);
|
||||
}
|
||||
|
||||
export default NetworkFormField
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useField, useFormikContext } from "formik";
|
||||
import { ChangeEvent, FC, forwardRef } from "react";
|
||||
import { SwapFormValues } from "../DTOs/SwapFormValues";
|
||||
import { classNames } from '../utils/classNames'
|
||||
|
||||
type Input = {
|
||||
label?: JSX.Element | JSX.Element[]
|
||||
pattern?: string;
|
||||
disabled?: boolean;
|
||||
placeholder: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
precision?: number;
|
||||
step?: number;
|
||||
name: string;
|
||||
className?: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
ref?: any;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
// Use with Formik
|
||||
const NumericInput: FC<Input> = forwardRef<HTMLInputElement, Input>(
|
||||
function NumericInput({ label, pattern, disabled, placeholder, min, max, minLength, maxLength, precision, step, name, className, children, onChange }, ref) {
|
||||
const { handleChange } = useFormikContext<SwapFormValues>();
|
||||
const [field] = useField(name)
|
||||
|
||||
return <>
|
||||
{label &&
|
||||
<label htmlFor={name} className="block font-semibold text-secondary-text text-sm mb-1.5 w-full">
|
||||
{label}
|
||||
</label>
|
||||
}
|
||||
<div className="flex rounded-lg shadow-sm bg-secondary-700 border-secondary-500 border ">
|
||||
<input
|
||||
{...field}
|
||||
pattern={pattern ? pattern : "^[0-9]*[.,]?[0-9]*$"}
|
||||
inputMode="decimal"
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoCorrect="off"
|
||||
min={min}
|
||||
max={max}
|
||||
minLength={minLength}
|
||||
maxLength={maxLength}
|
||||
onInput={(event: React.ChangeEvent<HTMLInputElement>) => { replaceComma(event); limitDecimalPlaces(event, precision) }}
|
||||
type="text"
|
||||
step={step}
|
||||
name={name}
|
||||
id={name}
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'disabled:cursor-not-allowed h-12 leading-4 placeholder:text-primary-text-placeholder bg-secondary-700 focus:ring-primary focus:border-primary flex-grow block w-full min-w-0 rounded-lg font-semibold border-0',
|
||||
className
|
||||
)}
|
||||
onChange={onChange ? onChange : e => {
|
||||
/^[0-9]*[.,]?[0-9]*$/.test(e.target.value) && handleChange(e);
|
||||
}}
|
||||
/>
|
||||
{children &&
|
||||
<span className="inline-flex items-center">
|
||||
{children}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</>;
|
||||
});
|
||||
|
||||
function limitDecimalPlaces(e, count) {
|
||||
if (e.target.value.indexOf('.') == -1) { return; }
|
||||
if ((e.target.value.length - e.target.value.indexOf('.')) > count) {
|
||||
e.target.value = ParseFloat(e.target.value, count);
|
||||
}
|
||||
}
|
||||
|
||||
function ParseFloat(str, val) {
|
||||
str = str.toString();
|
||||
str = str.slice(0, (str.indexOf(".")) + val + 1);
|
||||
return Number(str);
|
||||
}
|
||||
|
||||
function replaceComma(e) {
|
||||
var val = e.target.value;
|
||||
if (val.match(/\,/)) {
|
||||
val = val.replace(/\,/g, '.');
|
||||
e.target.value = val;
|
||||
}
|
||||
}
|
||||
|
||||
export default NumericInput
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ChevronRight, ExternalLink } from "lucide-react"
|
||||
import LinkWrapper from "../LinkWraapper"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
const Menu = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="flex flex-col gap-3 mt-3">
|
||||
{children}
|
||||
<div style={{ height: '70px' }} />
|
||||
</div>
|
||||
}
|
||||
|
||||
const Group = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="divide-y divide-secondary-500 rounded-md bg-secondary-700 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Item = (function Item({ children, pathname, onClick, icon, target = '_self' }: MenuIemProps) {
|
||||
|
||||
return (
|
||||
pathname ?
|
||||
<LinkWrapper href={pathname} target={target} className="gap-4 flex relative cursor-pointer hover:bg-secondary-600 select-none w-full items-center px-4 py-3 outline-none text-primary-text">
|
||||
<div>
|
||||
{icon}
|
||||
</div>
|
||||
<p className="text-primary-text">{children}</p>
|
||||
{
|
||||
target === '_self' ?
|
||||
<ChevronRight className="h-4 w-4 absolute right-3" />
|
||||
:
|
||||
<ExternalLink className="h-4 w-4 absolute right-3" />
|
||||
}
|
||||
</LinkWrapper>
|
||||
:
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`gap-4 flex relative cursor-pointer hover:bg-secondary-600 select-none items-center px-4 py-3 outline-none w-full text-primary-text`}
|
||||
>
|
||||
<div>
|
||||
{icon}
|
||||
</div>
|
||||
<p className="text-primary-text">{children}</p>
|
||||
<ChevronRight className="h-4 w-4 absolute right-3" />
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
export enum ItemType {
|
||||
button = 'button',
|
||||
link = 'link'
|
||||
}
|
||||
|
||||
type Target = '_blank' | '_self'
|
||||
|
||||
type MenuIemProps = {
|
||||
children: ReactNode;
|
||||
pathname?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
icon: JSX.Element;
|
||||
target?: Target;
|
||||
};
|
||||
|
||||
const Footer = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
|
||||
return (
|
||||
<div className={`text-primary-text text-base border-t border-secondary-500 fixed inset-x-0 bottom-0 z-30 bg-secondary-900 shadow-widget-footer px-6 py-4 w-full `}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Menu.Group = Group
|
||||
Menu.Item = Item
|
||||
Menu.Footer = Footer
|
||||
|
||||
export default Menu
|
||||
@@ -0,0 +1,240 @@
|
||||
import { BookOpen, Gift, MenuIcon, Map, Home, LogIn, ScrollText, LibraryIcon, Shield, Users, MessageSquarePlus, UserCircle2 } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useAuthDataUpdate, useAuthState, UserType } from "../../context/authContext";
|
||||
import TokenService from "../../lib/TokenService";
|
||||
import { useIntercom } from "react-use-intercom";
|
||||
import ChatIcon from "../icons/ChatIcon";
|
||||
import inIframe from "../utils/inIframe";
|
||||
import Modal from "../../components/modal/modal";
|
||||
import DiscordLogo from "./../icons/DiscordLogo";
|
||||
import GitHubLogo from "./../icons/GitHubLogo";
|
||||
import SubstackLogo from "./../icons/SubstackLogo";
|
||||
import TwitterLogo from "./../icons/TwitterLogo";
|
||||
import Link from "next/link";
|
||||
import Popover from "../modal/popover";
|
||||
import SendFeedback from "../sendFeedback";
|
||||
import IconButton from "../buttons/iconButton";
|
||||
import YoutubeLogo from "../icons/YoutubeLogo";
|
||||
import { shortenEmail } from '../utils/ShortenAddress';
|
||||
import { resolvePersistantQueryParams } from "../../helpers/querryHelper";
|
||||
import Menu from "./Menu";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const WalletsMenu = dynamic(() => import("../ConnectedWallets").then((comp) => comp.WalletsMenu), {
|
||||
loading: () => <></>
|
||||
})
|
||||
|
||||
export default function BridgeMenu() {
|
||||
const { email, userType, userId } = useAuthState()
|
||||
const { setUserType } = useAuthDataUpdate()
|
||||
const router = useRouter();
|
||||
const { boot, show, update } = useIntercom()
|
||||
const [embedded, setEmbedded] = useState<boolean>()
|
||||
const [openTopModal, setOpenTopModal] = useState(false);
|
||||
const [openFeedbackModal, setOpenFeedbackModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setEmbedded(inIframe())
|
||||
}, [])
|
||||
|
||||
const updateWithProps = () => update({ email: email, userId: userId })
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
TokenService.removeAuthData()
|
||||
if (router.pathname != '/') {
|
||||
router.push({
|
||||
pathname: '/',
|
||||
query: resolvePersistantQueryParams(router.query)
|
||||
})
|
||||
} else {
|
||||
router.reload()
|
||||
}
|
||||
setUserType(UserType.NotAuthenticatedUser)
|
||||
}, [router.query])
|
||||
|
||||
const navigation = {
|
||||
social: [
|
||||
{
|
||||
name: 'Twitter',
|
||||
href: 'https://twitter.com/bridge',
|
||||
icon: (props) => TwitterLogo(props),
|
||||
className: 'plausible-event-name=Twitter'
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
href: 'https://github.com/bridge/bridgeapp',
|
||||
icon: (props) => GitHubLogo(props),
|
||||
className: 'plausible-event-name=GitHub'
|
||||
},
|
||||
{
|
||||
name: 'Discord',
|
||||
href: 'https://discord.com/invite/KhwYN35sHy',
|
||||
icon: (props) => DiscordLogo(props),
|
||||
className: 'plausible-event-name=Discord'
|
||||
},
|
||||
{
|
||||
name: 'YouTube',
|
||||
href: 'https://www.youtube.com/@bridgehq',
|
||||
icon: (props) => YoutubeLogo(props),
|
||||
className: 'plausible-event-name=Youtube'
|
||||
},
|
||||
{
|
||||
name: 'Substack',
|
||||
href: 'https://bridge.substack.com/',
|
||||
icon: (props) => SubstackLogo(props),
|
||||
className: 'plausible-event-name=Substack'
|
||||
},
|
||||
{
|
||||
name: 'Roadmap',
|
||||
href: 'https://bridge.ducalis.io/roadmap/summary',
|
||||
icon: (props) => <Map {...props}></Map>,
|
||||
className: 'plausible-event-name=Roadmap'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const handleCloseFeedback = () => {
|
||||
setOpenFeedbackModal(false)
|
||||
}
|
||||
|
||||
return <>
|
||||
<span className="text-secondary-text cursor-pointer relative">
|
||||
{
|
||||
|
||||
<>
|
||||
<div className="relative top-">
|
||||
|
||||
<IconButton onClick={() => setOpenTopModal(true)} icon={
|
||||
<MenuIcon strokeWidth={3} />
|
||||
}>
|
||||
</IconButton>
|
||||
|
||||
</div>
|
||||
<Modal show={openTopModal} setShow={setOpenTopModal} header={<h2 className="font-normal leading-none tracking-tight">Menu</h2>}>
|
||||
<div className="text-sm font-medium focus:outline-none h-full">
|
||||
<Menu>
|
||||
|
||||
<WalletsMenu />
|
||||
|
||||
<Menu.Group>
|
||||
<>
|
||||
{
|
||||
router.pathname != '/' &&
|
||||
<Menu.Item pathname='/' icon={<Home className="h-5 w-5" />} >
|
||||
Home
|
||||
</Menu.Item>
|
||||
}
|
||||
</>
|
||||
<>
|
||||
{
|
||||
router.pathname != '/transactions' &&
|
||||
<Menu.Item pathname='/transactions' icon={<ScrollText className="h-5 w-5" />} >
|
||||
Transfers
|
||||
</Menu.Item>
|
||||
}
|
||||
</>
|
||||
<>
|
||||
{!embedded && router.pathname != '/campaigns' &&
|
||||
<Menu.Item pathname='/campaigns' icon={<Gift className="h-5 w-5" />} >
|
||||
Campaigns
|
||||
</Menu.Item>
|
||||
}
|
||||
</>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.Item onClick={() => {
|
||||
boot();
|
||||
show();
|
||||
updateWithProps();
|
||||
}} target="_blank" icon={<ChatIcon strokeWidth={2} className="h-5 w-5" />} >
|
||||
Help
|
||||
</Menu.Item>
|
||||
<Menu.Item pathname='https://docs.bridge.lux.network/' target="_blank" icon={<BookOpen className="h-5 w-5" />} >
|
||||
Docs for Users
|
||||
</Menu.Item>
|
||||
<Menu.Item pathname='https://docs.bridge.lux.network/user-docs/partners-and-integrations' target="_blank" icon={<Users className="h-5 w-5" />} >
|
||||
Docs for Partners
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
|
||||
<Menu.Group>
|
||||
<Menu.Item pathname='https://docs.bridge.lux.network/user-docs/information/privacy-policy' target="_blank" icon={<Shield className="h-5 w-5" />} >
|
||||
Privacy Policy
|
||||
</Menu.Item>
|
||||
<Menu.Item pathname='https://docs.bridge.lux.network/user-docs/information/terms-of-services' target="_blank" icon={<LibraryIcon className="h-5 w-5" />} >
|
||||
Terms of Service
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
|
||||
<Menu.Group>
|
||||
<Popover
|
||||
opener={
|
||||
<Menu.Item onClick={() => setOpenFeedbackModal(true)} target="_blank" icon={<MessageSquarePlus className="h-5 w-5" />} >
|
||||
Suggest a Feature
|
||||
</Menu.Item>
|
||||
}
|
||||
isNested={true}
|
||||
show={openFeedbackModal}
|
||||
header="Suggest a Feature"
|
||||
setShow={setOpenFeedbackModal} >
|
||||
<div className="p-0 md:max-w-md">
|
||||
<SendFeedback onSend={handleCloseFeedback} />
|
||||
</div>
|
||||
</Popover>
|
||||
</Menu.Group>
|
||||
|
||||
<div className="space-y-3 w-full">
|
||||
<hr className="border-secondary-500" />
|
||||
<p className="text-primary-text-muted flex justify-center my-3">Media links & suggestions:</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 justify-center">
|
||||
{navigation.social.map((item, index) => (
|
||||
<Link key={index} target="_blank" href={item.href} className={`flex relative bg-secondary-700 hover:bg-secondary-600 rounded-md cursor-pointer select-none items-center outline-none text-primary-text ${item.className}`}>
|
||||
<div className="p-2 w-full flex justify-center gap-1">
|
||||
<item.icon className="h-5 w-5" aria-hidden="true" />
|
||||
<p>{item.name}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{
|
||||
router.pathname != '/auth' &&
|
||||
<Menu.Footer>
|
||||
<Menu.Group>
|
||||
{
|
||||
userType == UserType.AuthenticatedUser ?
|
||||
<div>
|
||||
<div
|
||||
className={`gap-4 flex justify-between items-center relative select-none px-4 py-3 outline-none w-full text-primary-text`}
|
||||
>
|
||||
<div className="font-normal flex gap-2 items-center">
|
||||
<UserCircle2 className="h-5 w-5" />
|
||||
<p>{email && <UserEmail email={email} />}</p>
|
||||
</div>
|
||||
<button type="button" className="text-primary hover:text-primary-600" onClick={handleLogout}>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<Menu.Item pathname='/auth' icon={<LogIn className="h-5 w-5" />} >
|
||||
Sign in
|
||||
</Menu.Item>
|
||||
}
|
||||
</Menu.Group>
|
||||
</Menu.Footer>
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
</span >
|
||||
</>
|
||||
}
|
||||
|
||||
const UserEmail = ({ email }: { email: string }) => {
|
||||
return shortenEmail(email, 22)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import Link, { LinkProps } from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { FC } from "react";
|
||||
import { resolvePersistantQueryParams } from "../helpers/querryHelper";
|
||||
|
||||
const LinkWrapper: FC<Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> & LinkProps & {
|
||||
children?: React.ReactNode;
|
||||
} & React.RefAttributes<HTMLAnchorElement>> = (props) => {
|
||||
const router = useRouter();
|
||||
const { children } = props
|
||||
|
||||
const pathname = typeof props.href === 'object' ? props.href.pathname : props.href
|
||||
const query = (typeof props.href === 'object' && typeof props.href.query === 'object' && props.href.query) || {}
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
href={{
|
||||
pathname: pathname,
|
||||
query: {
|
||||
...resolvePersistantQueryParams(router.query),
|
||||
...query
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkWrapper
|
||||
@@ -0,0 +1,28 @@
|
||||
import { FC, useEffect } from "react"
|
||||
import { useLoadingState } from "../context/loadingContext"
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
}
|
||||
const LoadingCard: FC<Props> = ({ name }) => {
|
||||
return <div
|
||||
className={`bg-secondary-900 md:shadow-card rounded-lg w-full sm:overflow-hidden relative`}
|
||||
>
|
||||
<div className='text-center text-xl text-secondary-100'>
|
||||
</div>
|
||||
<div className="relative px-6">
|
||||
<div className="flex items-start">
|
||||
<div className={`flex flex-nowrap grow`}>
|
||||
<div className="w-full pb-6 flex flex-col justify-between space-y-5 text-secondary-text h-full">
|
||||
<div className="sm:min-h-[504px] flex flex-col justify-between">
|
||||
<div className="relative m-auto origin-center">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="widget_root" />
|
||||
</div>
|
||||
}
|
||||
export default LoadingCard
|
||||
@@ -0,0 +1,84 @@
|
||||
import CancelIcon from "./icons/CancelIcon";
|
||||
import DelayIcon from "./icons/DelayIcon";
|
||||
import FailIcon from "./icons/FailIcon";
|
||||
import SuccessIcon from "./icons/SuccessIcon";
|
||||
type iconStyle = 'red' | 'green' | 'yellow' | 'gray'
|
||||
|
||||
class MessageComponentProps {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
center?: boolean
|
||||
icon: iconStyle
|
||||
}
|
||||
|
||||
function constructIcons(icon: iconStyle) {
|
||||
|
||||
let iconStyle: JSX.Element
|
||||
|
||||
switch (icon) {
|
||||
case 'red':
|
||||
iconStyle = <FailIcon />;
|
||||
break;
|
||||
case 'green':
|
||||
iconStyle = <SuccessIcon />;
|
||||
break;
|
||||
case 'yellow':
|
||||
iconStyle = <DelayIcon />
|
||||
break
|
||||
case 'gray':
|
||||
iconStyle = CancelIcon
|
||||
break
|
||||
}
|
||||
return iconStyle
|
||||
}
|
||||
|
||||
const MessageComponent = ({ children }) => {
|
||||
return <div className="w-full flex flex-col h-full justify-between pt-6 min-h-full">
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
const Content = ({ children, icon, center }: MessageComponentProps) => {
|
||||
return (
|
||||
center ?
|
||||
<div className='flex flex-col self-center grow w-full'>
|
||||
<div className='flex self-center grow w-full'>
|
||||
<div className='flex flex-col space-y-8 self-center w-full'>
|
||||
<div className='flex place-content-center'>{constructIcons(icon)}</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className='space-y-3'>
|
||||
<div className='flex place-content-center'>{constructIcons(icon)}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ children }) => {
|
||||
return <div className='md:text-3xl text-lg font-bold text-primary-text leading-6 text-center'>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
const Description = ({ children }) => {
|
||||
return <div className="text-base font-medium space-y-6 text-secondary-text text-center mb-6">
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
const Buttons = ({ children }) => {
|
||||
return <div className="space-y-3">
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
MessageComponent.Content = Content
|
||||
MessageComponent.Header = Header
|
||||
MessageComponent.Description = Description
|
||||
MessageComponent.Buttons = Buttons
|
||||
|
||||
export default MessageComponent
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import MessageComponent from "./MessageComponent";
|
||||
import Navbar from "./navbar";
|
||||
import inIframe from "./utils/inIframe";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
function NoCookies(props) {
|
||||
const [embedded, setEmbedded] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
setEmbedded(inIframe())
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="styled-scroll">
|
||||
<div className="min-h-screen overflow-hidden relative font-robo">
|
||||
<div className="mx-auto max-w-xl bg-secondary-900 shadow-card rounded-lg w-full overflow-hidden relative px-0 md:px-8 py-6 h-[500px] min-h-[550px]">
|
||||
<MessageComponent>
|
||||
<MessageComponent.Content icon="red">
|
||||
<MessageComponent.Header>
|
||||
Sorry
|
||||
</MessageComponent.Header>
|
||||
<MessageComponent.Description>
|
||||
<div className="text-secondary-text space-y-5 text-left">
|
||||
<div className="space-y-2">
|
||||
<p className="text-primary-text">
|
||||
It seems like you’ve either:
|
||||
</p>
|
||||
<ul className="text-secondary-text list-disc ml-4 mt-0 ">
|
||||
<li>Disabled cookies</li>
|
||||
<li>Or using Bridge in a partner’s page in Incognito mode</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="text-secondary-text">Unforunately, we can’t run in those conditions 🙁</p>
|
||||
</div>
|
||||
{
|
||||
embedded &&
|
||||
<Link target="_blank" href={window?.location?.href} className="bg-primary text-primary-buttonTextColor py-3 px-3 border border-primary disabled:border-primary-900 shadowed-button items-center space-x-1 disabled:text-opacity-40 disabled:bg-primary-900 disabled:cursor-not-allowed relative w-full flex justify-center font-semibold rounded-md shadow-md hover:shadow-xl transform hover:-translate-y-0.5 transition duration-200 ease-in-out">
|
||||
Try on Bridge
|
||||
</Link>
|
||||
}
|
||||
</MessageComponent.Description>
|
||||
</MessageComponent.Content>
|
||||
</MessageComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
export default NoCookies;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import { SwapType } from "../lib/BridgeApiClient";
|
||||
import { classNames } from "./utils/classNames";
|
||||
|
||||
export interface NavRadioOption {
|
||||
value: string;
|
||||
isEnabled: boolean;
|
||||
isHighlighted: boolean;
|
||||
}
|
||||
|
||||
export interface NavRadioProps {
|
||||
label: string,
|
||||
value: string,
|
||||
items: NavRadioOption[],
|
||||
setSelected: (value: string) => void,
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const OptionToggle: FC<NavRadioProps> = ({ value, items, setSelected, label, disabled }) => {
|
||||
|
||||
const onchange = (item: NavRadioOption) => {
|
||||
setSelected(item.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<RadioGroup value={items.find(i => i.value === value)} disabled={disabled} onChange={onchange} className="mt-2 w-full my-4">
|
||||
<RadioGroup.Label className="font-normal text-secondary-text text-sm">{label}</RadioGroup.Label>
|
||||
<div className={`grid grid-cols-${items?.length} gap-1 md:gap-2 p-0.5 md:p-2 rounded-lg bg-secondary-700 border-secondary-500 border`}>
|
||||
{items.map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={option.value}
|
||||
value={option}
|
||||
className={({ checked }) =>
|
||||
classNames(
|
||||
option.isEnabled ? 'cursor-pointer focus:outline-none' : 'opacity-25 cursor-not-allowed',
|
||||
checked
|
||||
? 'bg-secondary-500 border-transparent text-primary-text'
|
||||
: 'bg-transparent border-transparent text-gray-400 hover:text-gray-200',
|
||||
`border rounded-md p-1 flex items-center justify-center text-sm font-medium sm:flex-1 relative`
|
||||
)
|
||||
}
|
||||
disabled={!option.isEnabled}>
|
||||
{
|
||||
option.value === SwapType.OnRamp &&
|
||||
<div className="flex items-center md:p-0 p-1.5 text-xs md:text-base">
|
||||
On-Ramp
|
||||
</div>
|
||||
}
|
||||
{
|
||||
option.value === SwapType.OffRamp &&
|
||||
<div className="flex items-center space-x-1 md:p-0 p-1.5 text-xs md:text-base ">
|
||||
Off-Ramp
|
||||
</div>
|
||||
}
|
||||
{
|
||||
option.value === SwapType.CrossChain &&
|
||||
<div className="inline-flex items-center ">
|
||||
<span className="flex items-center space-x-1 md:p-0 p-1.5 text-xs md:text-base">
|
||||
Cross-Chain
|
||||
</span>
|
||||
<span className="absolute ml-1 -top-1 -right-2 shadow-sm rounded bg-primary px-2 text-[10px] leading-4 font-semibold text-primary-text"> New </span>
|
||||
</div>
|
||||
|
||||
}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
export default OptionToggle
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import { classNames } from "./utils/classNames"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 transition-all bg-primary"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FC, useState } from "react"
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { classNames } from "./utils/classNames";
|
||||
import { QrCode } from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./shadcn/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./shadcn/tooltip";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type QRCodeModalProps = {
|
||||
qrUrl: string;
|
||||
className?: string
|
||||
iconSize?: number
|
||||
iconClassName?: string
|
||||
}
|
||||
|
||||
const QRCodeModal: FC<QRCodeModalProps> = ({ qrUrl, className, iconSize, iconClassName }) => {
|
||||
const qrCode =
|
||||
<QRCodeSVG
|
||||
className="rounded-lg"
|
||||
value={qrUrl}
|
||||
includeMargin={true}
|
||||
size={160}
|
||||
level={"H"}
|
||||
/>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={classNames(className)}>
|
||||
<div className="flex items-center gap-1 cursor-pointer">
|
||||
<QrCode className={iconClassName} width={iconSize ? iconSize : 16} height={iconSize ? iconSize : 16} />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Show QR code</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-4" side="left">
|
||||
<motion.div whileHover={{
|
||||
scale: 1.2,
|
||||
transition: { duration: 0.5 },
|
||||
}}>
|
||||
{qrCode}
|
||||
</motion.div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default QRCodeModal
|
||||
@@ -0,0 +1,106 @@
|
||||
import "@rainbow-me/rainbowkit/styles.css";
|
||||
import {
|
||||
darkTheme,
|
||||
connectorsForWallets,
|
||||
RainbowKitProvider,
|
||||
DisclaimerComponent,
|
||||
AvatarComponent
|
||||
} from '@rainbow-me/rainbowkit';
|
||||
const WALLETCONNECT_PROJECT_ID = '28168903b2d30c75e5f7f2d71902581b';
|
||||
import { publicProvider } from 'wagmi/providers/public';
|
||||
import { walletConnectWallet, rainbowWallet, metaMaskWallet, coinbaseWallet, bitgetWallet, argentWallet } from '@rainbow-me/rainbowkit/wallets';
|
||||
import { useSettingsState } from "../context/settings";
|
||||
import { Chain, WagmiConfig, configureChains, createConfig } from "wagmi";
|
||||
import { NetworkType } from "../Models/CryptoNetwork";
|
||||
import resolveChain from "../lib/resolveChain";
|
||||
import React from "react";
|
||||
import AddressIcon from "./AddressIcon";
|
||||
import NoCookies from "./NoCookies";
|
||||
|
||||
type Props = {
|
||||
children: JSX.Element | JSX.Element[]
|
||||
}
|
||||
|
||||
function RainbowKitComponent({ children }: Props) {
|
||||
const settings = useSettingsState();
|
||||
|
||||
const isChain = (c: Chain | undefined): c is Chain => c != undefined
|
||||
const settingsChains = settings?.networks
|
||||
.sort((a, b) => Number(a.chain_id) - Number(b.chain_id))
|
||||
.filter(net => net.type === NetworkType.EVM
|
||||
&& net.nodes?.some(n => n.url?.length > 0))
|
||||
.map(resolveChain).filter(isChain) || []
|
||||
|
||||
const { chains, publicClient } = configureChains(
|
||||
settingsChains,
|
||||
[publicProvider()]
|
||||
);
|
||||
|
||||
const projectId = WALLETCONNECT_PROJECT_ID;
|
||||
const connectors = connectorsForWallets([
|
||||
{
|
||||
groupName: 'Popular',
|
||||
wallets: [
|
||||
metaMaskWallet({ projectId, chains }),
|
||||
walletConnectWallet({ projectId, chains }),
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: 'Wallets',
|
||||
wallets: [
|
||||
coinbaseWallet({ chains, appName: 'Bridge' }),
|
||||
argentWallet({ projectId, chains }),
|
||||
bitgetWallet({ projectId, chains }),
|
||||
rainbowWallet({ projectId, chains })
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const theme = darkTheme({
|
||||
accentColor: 'rgb(var(--ls-colors-primary-500))',
|
||||
accentColorForeground: 'rgb(var(--ls-colors-primary-text))',
|
||||
borderRadius: 'small',
|
||||
fontStack: 'system',
|
||||
overlayBlur: 'small',
|
||||
})
|
||||
|
||||
theme.colors.modalBackground = 'rgb(var(--ls-colors-secondary-900))'
|
||||
theme.colors.modalText = 'rgb(var(--ls-colors-primary-text))'
|
||||
theme.colors.modalTextSecondary = 'rgb(var(--ls-colors-secondary-text))'
|
||||
theme.colors.actionButtonBorder = 'rgb(var(--ls-colors-secondary-500))'
|
||||
theme.colors.actionButtonBorderMobile = 'rgb(var(--ls-colors-secondary-500))'
|
||||
theme.colors.closeButton = 'rgb(var(--ls-colors-secondary-text))'
|
||||
theme.colors.closeButtonBackground = 'rgb(var(--ls-colors-secondary-500))'
|
||||
theme.colors.generalBorder = 'rgb(var(--ls-colors-secondary-500))'
|
||||
|
||||
const wagmiConfig = createConfig({
|
||||
autoConnect: true,
|
||||
connectors,
|
||||
publicClient,
|
||||
})
|
||||
|
||||
const disclaimer: DisclaimerComponent = ({ Text }) => (
|
||||
<Text>
|
||||
Thanks for choosing Bridge!
|
||||
</Text>
|
||||
);
|
||||
|
||||
const CustomAvatar: AvatarComponent = ({ address, size }) => {
|
||||
return <AddressIcon address={address} size={size} />
|
||||
};
|
||||
|
||||
return (
|
||||
<WagmiConfig config={wagmiConfig}>
|
||||
<RainbowKitProvider avatar={CustomAvatar} modalSize="compact" chains={chains} theme={theme}
|
||||
appInfo={{
|
||||
appName: 'Bridge',
|
||||
learnMoreUrl: 'https://docs.bridge.lux.network/',
|
||||
disclaimer: disclaimer
|
||||
}}>
|
||||
{children}
|
||||
</RainbowKitProvider>
|
||||
</WagmiConfig>
|
||||
)
|
||||
}
|
||||
|
||||
export default RainbowKitComponent
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMemo } from "react"
|
||||
import { useBalancesState } from "../context/balances"
|
||||
import useWallet from "../hooks/useWallet"
|
||||
import WarningMessage from "./WarningMessage"
|
||||
import { useFormikContext } from "formik"
|
||||
import { SwapFormValues } from "./DTOs/SwapFormValues"
|
||||
import { truncateDecimals } from "./utils/RoundDecimals"
|
||||
import { CalculateMinAllowedAmount } from "../lib/fees"
|
||||
import { useSettingsState } from "../context/settings"
|
||||
import { Balance, Gas } from "../Models/Balance"
|
||||
|
||||
|
||||
const ReserveGasNote = ({ onSubmit }: { onSubmit: (walletBalance: Balance, networkGas: Gas) => void }) => {
|
||||
const {
|
||||
values,
|
||||
} = useFormikContext<SwapFormValues>();
|
||||
const { balances, gases } = useBalancesState()
|
||||
const settings = useSettingsState()
|
||||
const { getWithdrawalProvider: getProvider } = useWallet()
|
||||
const provider = useMemo(() => {
|
||||
return values.from && getProvider(values.from)
|
||||
}, [values.from, getProvider])
|
||||
|
||||
const wallet = provider?.getConnectedWallet()
|
||||
const minAllowedAmount = CalculateMinAllowedAmount(values, settings.networks, settings.currencies);
|
||||
|
||||
const walletBalance = wallet && balances[wallet.address]?.find(b => b?.network === values?.from?.internal_name && b?.token === values?.currency?.asset)
|
||||
const networkGas = values.from?.internal_name ?
|
||||
gases?.[values.from?.internal_name]?.find(g => g?.token === values?.currency?.asset)
|
||||
: null
|
||||
|
||||
const mightBeAutOfGas = !!(networkGas && walletBalance?.isNativeCurrency && Number(values.amount)
|
||||
+ networkGas?.gas > walletBalance.amount
|
||||
&& walletBalance.amount > minAllowedAmount
|
||||
)
|
||||
const gasToReserveFormatted = mightBeAutOfGas ? truncateDecimals(networkGas?.gas, values?.currency?.precision) : 0
|
||||
|
||||
return (
|
||||
mightBeAutOfGas && gasToReserveFormatted > 0 &&
|
||||
<WarningMessage messageType="warning" className="mt-4">
|
||||
<div className="font-normal text-primary-text">
|
||||
<div>
|
||||
You might not be able to complete the transaction.
|
||||
</div>
|
||||
<div onClick={() => onSubmit(walletBalance, networkGas)} className="cursor-pointer border-b border-dotted border-primary-text w-fit hover:text-primary hover:border-primary text-primary-text">
|
||||
<span>Reserve</span> <span>{gasToReserveFormatted}</span> <span>{values?.currency?.asset}</span> <span>for gas.</span>
|
||||
</div>
|
||||
</div>
|
||||
</WarningMessage>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReserveGasNote
|
||||
@@ -0,0 +1,48 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ReactNode } from "react";
|
||||
import { useMeasure } from "@uidotdev/usehooks";
|
||||
|
||||
export default function ResizablePanel({ children, className }: { children: ReactNode, className?: string }) {
|
||||
let [ref, { height }] = useMeasure();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ height: height || "auto" }}
|
||||
className="relative overflow-hidden"
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
<motion.div
|
||||
key={JSON.stringify(children, ignoreCircularReferences())}
|
||||
initial={{
|
||||
x: 382,
|
||||
}}
|
||||
animate={{
|
||||
x: 0,
|
||||
}}
|
||||
exit={{
|
||||
x: -382,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const ignoreCircularReferences = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (key.startsWith("_")) return; // Don't compare React's internal props.
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) return;
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,329 @@
|
||||
import { ChevronRight, Clock } from "lucide-react"
|
||||
import BackgroundField from "./backgroundField"
|
||||
import { classNames } from "./utils/classNames"
|
||||
|
||||
export const SwapHistoryComponentSceleton = () => {
|
||||
|
||||
return <div className="animate-pulse">
|
||||
<div className=" mb-10 ">
|
||||
<div className="-mx-4 mt-10 sm:-mx-6 md:mx-0 md:rounded-lg ">
|
||||
<table className="min-w-full divide-y divide-secondary-500">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-500 sm:pl-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="hidden lg:block">
|
||||
<div className="h-2 w-8 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-500 lg:table-cell"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-8 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-500 "
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-8 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-500 lg:table-cell"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-500 lg:table-cell"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th
|
||||
scope="col"
|
||||
className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-500 lg:table-cell"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-8 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-8 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(5)]?.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'relative py-4 pl-4 sm:pl-6 pr-3 text-sm'
|
||||
)}
|
||||
>
|
||||
<div className="text-primary-text hidden lg:block">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
{index !== 0 ? <div className="absolute right-0 left-6 -top-px h-px bg-secondary-500" /> : null}
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'hidden px-3 py-3.5 text-sm text-primary-text lg:table-cell'
|
||||
)}
|
||||
>
|
||||
<div className="flex space-x-2">
|
||||
<div className="rounded-full bg-slate-700 h-4 w-4"></div>
|
||||
<div className="grid grid-cols-4 items-center">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'px-3 py-3.5 text-sm text-primary-text table-cell'
|
||||
)}
|
||||
>
|
||||
<div className="flex space-x-2">
|
||||
<div className="rounded-full bg-slate-700 h-4 w-4"></div>
|
||||
<div className="grid grid-cols-4 items-center">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'hidden px-3 py-3.5 text-sm text-primary-text lg:table-cell'
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'relative px-3 py-3.5 text-sm text-primary-text'
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'px-3 py-3.5 text-sm text-primary-text hidden lg:table-cell'
|
||||
)}
|
||||
>
|
||||
<div className="flex space-x-2">
|
||||
<div className="rounded bg-slate-700 h-2 w-2"></div>
|
||||
<div className="grid grid-cols-1 items-center">
|
||||
<div className="h-2 w-16 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'px-3 py-3.5 text-sm text-primary-text hidden lg:table-cell'
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="h-2 w-12 bg-slate-700 rounded col-span-1"></div>
|
||||
<div className="h-2 w-8 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={classNames(
|
||||
index === 0 ? '' : 'border-t border-secondary-500',
|
||||
'px-3 py-3.5 text-sm text-primary-text hidden lg:table-cell'
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<ChevronRight className="h-5 w-5 text-slate-700" />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
export const SwapDetailsComponentSceleton = () => {
|
||||
return <div className="animate-pulse"><div className="w-full grid grid-flow-row">
|
||||
<div className="rounded-md bg-secondary-900 w-full grid grid-flow-row">
|
||||
<div className="items-center block text-base font-lighter leading-6 text-secondary-text">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<div className="h-2 m-2 w-1/4 bg-slate-400 rounded col-span-1"></div>
|
||||
<div className="h-2 m-2 w-1/4 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
{[...Array(8)]?.map((item, index) => (
|
||||
<div key={index}>
|
||||
<hr className='horizontal-gradient my-1' />
|
||||
<div className="flex justify-between items-baseline">
|
||||
<div className="h-2.5 m-2 w-1/4 bg-slate-700 rounded-full col-span-1"></div>
|
||||
<div className="h-2 m-2 w-1/4 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export const DocInFrameSceleton = () => {
|
||||
return <div className="shadow rounded-md w-full mx-auto px-2 md:px-4">
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="flex-1 items-center space-y-6 py-1 content-start">
|
||||
<div className="h-4 mx-auto w-1/2 place-self-center justify-self-center self-center bg-slate-700 rounded mb-4"></div>
|
||||
<div className="space-y-6">
|
||||
{[...Array(8)]?.map((item, index) =>
|
||||
<div className="space-y-4"
|
||||
key={index}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
|
||||
<div className="h-2 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export const ExchangesComponentSceleton = () => {
|
||||
|
||||
return <>
|
||||
{[...Array(12)]?.map((item, index) =>
|
||||
<div
|
||||
key={index}
|
||||
className="animate-pulse bg-secondary-700 select-none rounded-lg py-5 px-3">
|
||||
<div className="flex justify-between space-x-4 md:space-x-16 px-3">
|
||||
<div className="flex space-x-2">
|
||||
<div className="rounded-md bg-slate-700 h-8 w-8"></div>
|
||||
<div className="grid grid-cols-5">
|
||||
<div className="h-2 w-20 bg-slate-700 rounded col-span-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-slate-700 h-8 w-20 place-self-end py-3 px-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
export const RewardsComponentSceleton = () => {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-md w-48 bg-gray-500 h-[28px] animate-pulse" />
|
||||
</div>
|
||||
<div className=" bg-secondary-700 divide-y divide-secondary-500 rounded-lg shadow-lg border border-secondary-700 hover:border-secondary-500 transition duration-200">
|
||||
<BackgroundField header={<span className="flex justify-between"><span>Pending Earnings</span><span>Next Airdrop</span></span>} withoutBorder>
|
||||
<div className="flex justify-between w-full text-2xl">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-32 h-6 rounded-md animate-pulse bg-gray-500" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="h-5" />
|
||||
<div className="w-14 h-6 rounded-md animate-pulse bg-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
</BackgroundField>
|
||||
<BackgroundField header={<span className="flex justify-between"><span>Total Earnings</span><span>Current Value</span></span>} withoutBorder>
|
||||
<div className="flex justify-between w-full text-slate-300 text-2xl">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-40 h-6 rounded-md animate-pulse bg-gray-500" />
|
||||
</div>
|
||||
<div className="w-20 h-6 rounded-md animate-pulse bg-gray-500" />
|
||||
</div>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
<div className="bg-secondary-700 rounded-lg shadow-lg border border-secondary-700 hover:border-secondary-500 transition duration-200">
|
||||
<BackgroundField header='Daily Reward Claimed' withoutBorder>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<div className="rounded-full h-4 bg-gray-500 w-full animate-pulse" />
|
||||
<div className="flex justify-between w-full font-semibold text-sm ">
|
||||
<div className="rounded-md w-20 h-5 animate-pulse bg-gray-500" />
|
||||
<div className="rounded-md w-32 h-5 animate-pulse bg-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-md w-48 bg-gray-500 h-5 animate-pulse" />
|
||||
</div>
|
||||
<div className=" bg-secondary-700 rounded-lg shadow-lg border border-secondary-700 hover:border-secondary-500 transition duration-200">
|
||||
<div className="p-3">
|
||||
<div className="space-y-6">
|
||||
{[...Array(4)]?.map((user, index) => (
|
||||
<div key={index} className="items-center flex justify-between">
|
||||
<div className="w-full h-4 rounded-md bg-gray-500 animate-pulse" />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const RewardsComponentLeaderboardSceleton = () => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-md w-48 bg-gray-500 h-5 animate-pulse" />
|
||||
</div>
|
||||
<div className="bg-secondary-700 border border-secondary-700 hover:border-secondary-500 transition duration-200 rounded-lg">
|
||||
<div className="p-3">
|
||||
<div className="space-y-6">
|
||||
{[...Array(4)]?.map((user, index) => (
|
||||
<div key={index} className="items-center flex justify-between">
|
||||
<div className="w-full h-4 rounded-md bg-gray-500 animate-pulse" />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import {ChevronDown } from 'lucide-react'
|
||||
import { ISelectMenuItem, SelectMenuItem } from '../Shared/Props/selectMenuItem'
|
||||
import CommandSelect, { SelectMenuItemGroup } from './commandSelect'
|
||||
|
||||
type CommandSelectWrapperProps = {
|
||||
setValue: (value: ISelectMenuItem) => void;
|
||||
values: ISelectMenuItem[];
|
||||
value?: ISelectMenuItem;
|
||||
placeholder: string;
|
||||
searchHint: string;
|
||||
disabled: boolean;
|
||||
valueGrouper: (values: ISelectMenuItem[]) => SelectMenuItemGroup[];
|
||||
}
|
||||
|
||||
export default function CommandSelectWrapper<T>({
|
||||
setValue,
|
||||
value,
|
||||
disabled,
|
||||
placeholder,
|
||||
searchHint,
|
||||
values,
|
||||
valueGrouper
|
||||
}: CommandSelectWrapperProps) {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
function openModal() {
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleSelect = useCallback((item: SelectMenuItem<T>) => {
|
||||
setValue(item)
|
||||
setShowModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openModal}
|
||||
disabled={disabled}
|
||||
className="rounded-lg focus-peer:ring-primary focus-peer:border-secondary-400 focus-peer:border focus-peer:ring-1 focus:outline-none disabled:cursor-not-allowed relative grow h-12 flex items-center text-left justify-bottom w-full pl-3 pr-2 py-2 bg-secondary-600 border border-secondary-500 font-semibold"
|
||||
>
|
||||
<span className='flex grow text-left items-center'>
|
||||
{
|
||||
value && <div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-6 w-6 relative">
|
||||
{
|
||||
value.imgSrc && <Image
|
||||
src={value.imgSrc}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
priority
|
||||
className="rounded-md object-contain"
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{value
|
||||
?
|
||||
<span className="ml-3 block font-medium text-primary-text flex-auto items-center">
|
||||
{value?.name}
|
||||
</span>
|
||||
:
|
||||
<span className="block font-medium text-primary-text-placeholder flex-auto items-center">
|
||||
{placeholder}
|
||||
</span>}
|
||||
</span>
|
||||
<span className="ml-3 right-0 flex items-center pr-2 pointer-events-none text-primary-text">
|
||||
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<CommandSelect
|
||||
setShow={setShowModal}
|
||||
setValue={handleSelect}
|
||||
show={showModal}
|
||||
value={value}
|
||||
searchHint={searchHint}
|
||||
valueGrouper={valueGrouper}
|
||||
values={values}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ISelectMenuItem } from '../Shared/Props/selectMenuItem'
|
||||
import {
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandWrapper
|
||||
} from '../../shadcn/command'
|
||||
import React from "react";
|
||||
import useWindowDimensions from '../../../hooks/useWindowDimensions';
|
||||
import SelectItem from '../Shared/SelectItem';
|
||||
import { SelectProps } from '../Shared/Props/SelectProps'
|
||||
import Modal from '../../modal/modal';
|
||||
import { Info } from 'lucide-react';
|
||||
import { LayerDisabledReason } from '../Popover/PopoverSelect';
|
||||
|
||||
export interface CommandSelectProps extends SelectProps {
|
||||
show: boolean;
|
||||
setShow: (value: boolean) => void;
|
||||
searchHint: string;
|
||||
valueGrouper: (values: ISelectMenuItem[]) => SelectMenuItemGroup[];
|
||||
}
|
||||
|
||||
export class SelectMenuItemGroup {
|
||||
constructor(init?: Partial<SelectMenuItemGroup>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
name: string;
|
||||
items: ISelectMenuItem[];
|
||||
}
|
||||
|
||||
export default function CommandSelect({ values, setValue, show, setShow, searchHint, valueGrouper }: CommandSelectProps) {
|
||||
const { isDesktop } = useWindowDimensions();
|
||||
let groups: SelectMenuItemGroup[] = valueGrouper(values);
|
||||
return (
|
||||
<Modal height='full' show={show} setShow={setShow}>
|
||||
{show ?
|
||||
<CommandWrapper>
|
||||
<CommandInput autoFocus={isDesktop} placeholder={searchHint} />
|
||||
{
|
||||
values.some(v => v.isAvailable.value === false && v.isAvailable.disabledReason === LayerDisabledReason.LockNetworkIsTrue) &&
|
||||
<div className='text-xs text-left text-secondary-text mb-2'>
|
||||
<Info className='h-3 w-3 inline-block mb-0.5' /><span> You're accessing Bridge from a partner's page. In case you want to transact with other networks, please open bridge.lux.network in a separate tab.</span>
|
||||
</div>
|
||||
}
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{groups.filter(g => g.items?.length > 0).map((group) => {
|
||||
return (
|
||||
<CommandGroup key={group.name} heading={group.name}>
|
||||
{group.items.map(item =>
|
||||
<CommandItem disabled={!item.isAvailable.value} value={item.name} key={item.id} onSelect={() => {
|
||||
setValue(item)
|
||||
setShow(false)
|
||||
}}>
|
||||
<SelectItem item={item} />
|
||||
</CommandItem>)
|
||||
}
|
||||
</CommandGroup>)
|
||||
})}
|
||||
</CommandList>
|
||||
</CommandWrapper>
|
||||
: <></>
|
||||
}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { SelectProps } from '../Shared/Props/SelectProps'
|
||||
import { CommandItem, CommandList, CommandWrapper } from '../../shadcn/command';
|
||||
import SelectItem from '../Shared/SelectItem';
|
||||
|
||||
export default function PopoverSelect({ values, value, setValue }: SelectProps) {
|
||||
|
||||
return (
|
||||
<CommandWrapper>
|
||||
<CommandList>
|
||||
{values.map(item =>
|
||||
<CommandItem disabled={!item.isAvailable.value} value={item.id} key={item.id} onSelect={() => {
|
||||
setValue(item)
|
||||
}}>
|
||||
<SelectItem item={item} />
|
||||
</CommandItem>)
|
||||
}
|
||||
</CommandList>
|
||||
</CommandWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export enum LayerDisabledReason {
|
||||
LockNetworkIsTrue = '',
|
||||
InsufficientLiquidity = 'Temporarily disabled. Please check later.'
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { ISelectMenuItem, SelectMenuItem } from '../Shared/Props/selectMenuItem'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../shadcn/popover'
|
||||
import PopoverSelect from './PopoverSelect'
|
||||
|
||||
type PopoverSelectWrapper = {
|
||||
setValue: (value: ISelectMenuItem) => void;
|
||||
values: ISelectMenuItem[];
|
||||
value?: ISelectMenuItem;
|
||||
placeholder?: string;
|
||||
searchHint?: string;
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function PopoverSelectWrapper<T>({
|
||||
setValue,
|
||||
value,
|
||||
values,
|
||||
disabled
|
||||
}: PopoverSelectWrapper) {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
const handleSelect = useCallback((item: SelectMenuItem<T>) => {
|
||||
setValue(item)
|
||||
setShowModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
disabled ?
|
||||
<div className="relative">
|
||||
<div className='w-full py-0 px-3 md:px-5 border-transparent bg-transparent font-semibold rounded-md'>
|
||||
<span className="flex items-center">
|
||||
<div className="flex-shrink-0 h-6 w-6 relative">
|
||||
{
|
||||
value?.imgSrc && <Image
|
||||
src={value?.imgSrc}
|
||||
alt="Project Logo"
|
||||
priority
|
||||
height="40"
|
||||
width="40"
|
||||
className="rounded-md object-contain"
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
<span className="ml-3 block truncate text-primary-text">{value?.name}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<Popover open={showModal} onOpenChange={() => setShowModal(!showModal)}>
|
||||
<PopoverTrigger asChild>
|
||||
{
|
||||
value &&
|
||||
<div className="relative">
|
||||
<button type='button' className='w-full py-0 pl-3 md:pl-5 pr-10 border-transparent bg-transparent font-semibold rounded-md'>
|
||||
<span className="flex items-center">
|
||||
<div className="flex-shrink-0 h-6 w-6 relative">
|
||||
{
|
||||
value.imgSrc && <Image
|
||||
src={value.imgSrc}
|
||||
alt="Project Logo"
|
||||
priority
|
||||
height="40"
|
||||
width="40"
|
||||
className="rounded-md object-contain"
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
<span className="ml-3 block truncate text-primary-text">{value.name}</span>
|
||||
</span>
|
||||
|
||||
<span className="ml-3 absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-primary-text">
|
||||
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit">
|
||||
<PopoverSelect setValue={handleSelect} value={value} values={values} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ISelectMenuItem } from '../../Shared/Props/selectMenuItem'
|
||||
|
||||
export interface SelectProps {
|
||||
values: ISelectMenuItem[],
|
||||
value?: ISelectMenuItem;
|
||||
setValue: (value: ISelectMenuItem) => void;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { CurrencyDisabledReason } from "../../../Input/CurrencyFormField";
|
||||
import { LayerDisabledReason } from "../../Popover/PopoverSelect";
|
||||
|
||||
export class SelectMenuItem<T> implements ISelectMenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
order: number;
|
||||
imgSrc: string;
|
||||
isAvailable: {
|
||||
value: boolean;
|
||||
disabledReason: LayerDisabledReason | CurrencyDisabledReason | null
|
||||
};
|
||||
group?: string;
|
||||
details?: string;
|
||||
baseObject: T;
|
||||
constructor(baseObject: T, id: string, name: string, order: number, imgSrc: string, group?: string, details?: string) {
|
||||
this.baseObject = baseObject;
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.order = order;
|
||||
this.imgSrc = imgSrc;
|
||||
this.group = group;
|
||||
this.details = details
|
||||
this.isAvailable = {
|
||||
value: true,
|
||||
disabledReason: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISelectMenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
imgSrc: string;
|
||||
group?: string;
|
||||
isAvailable: {
|
||||
value: boolean;
|
||||
disabledReason: LayerDisabledReason | CurrencyDisabledReason | null
|
||||
};
|
||||
details?: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ISelectMenuItem } from "./Props/selectMenuItem";
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function SelectItem({ item }: { item: ISelectMenuItem }) {
|
||||
return (<div className="flex items-center w-full">
|
||||
<div className="flex-shrink-0 h-6 w-6 relative">
|
||||
{item.imgSrc && <Image
|
||||
src={item.imgSrc}
|
||||
alt="Project Logo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="eager"
|
||||
className="rounded-md object-contain" />}
|
||||
</div>
|
||||
<div className="ml-4 flex items-center gap-3 justify-between w-full">
|
||||
<p className='text-md font-medium'>
|
||||
{item.name}
|
||||
</p>
|
||||
{
|
||||
item.details &&
|
||||
<p className="text-primary-text-muted">
|
||||
{item.details}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { Album, ChevronDown, Mail, ScrollText, User } from 'lucide-react';
|
||||
import { Field, Form, Formik, FormikErrors } from 'formik';
|
||||
import { FC, useCallback } from 'react'
|
||||
import toast from 'react-hot-toast';
|
||||
import { useAuthDataUpdate, useAuthState } from '../context/authContext';
|
||||
import { useTimerState } from '../context/timerContext';
|
||||
import TokenService from '../lib/TokenService';
|
||||
import BridgeAuthApiClient from '../lib/userAuthApiClient';
|
||||
import SubmitButton from './buttons/submitButton';
|
||||
import { Widget } from './Widget/Index';
|
||||
|
||||
type EmailFormValues = {
|
||||
email: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onSend: (email: string) => void;
|
||||
disclosureLogin?: boolean;
|
||||
}
|
||||
|
||||
const SendEmail: FC<Props> = ({ onSend, disclosureLogin }) => {
|
||||
const { codeRequested, tempEmail, userType } = useAuthState()
|
||||
const { setCodeRequested, updateTempEmail } = useAuthDataUpdate();
|
||||
const initialValues: EmailFormValues = { email: tempEmail ?? "" };
|
||||
const { start: startTimer } = useTimerState()
|
||||
|
||||
const sendEmail = useCallback(async (values: EmailFormValues) => {
|
||||
try {
|
||||
const inputEmail = values.email;
|
||||
|
||||
if (inputEmail != tempEmail || !codeRequested) {
|
||||
|
||||
const apiClient = new BridgeAuthApiClient();
|
||||
const res = await apiClient.getCodeAsync(inputEmail)
|
||||
if (res.error)
|
||||
throw new Error(res.error)
|
||||
TokenService.setCodeNextTime(res?.data?.next)
|
||||
setCodeRequested(true);
|
||||
updateTempEmail(inputEmail)
|
||||
const next = new Date(res?.data?.next)
|
||||
const now = new Date()
|
||||
const miliseconds = next.getTime() - now.getTime()
|
||||
startTimer(Math.round((res?.data?.already_sent ? 60000 : miliseconds) / 1000))
|
||||
}
|
||||
onSend(inputEmail)
|
||||
}
|
||||
catch (error) {
|
||||
if (error.response?.data?.errors?.length > 0) {
|
||||
const message = error.response.data.errors.map(e => e.message).join(", ")
|
||||
toast.error(message)
|
||||
}
|
||||
else {
|
||||
toast.error(error.message)
|
||||
}
|
||||
}
|
||||
}, [tempEmail])
|
||||
|
||||
function validateEmail(values: EmailFormValues) {
|
||||
let error: FormikErrors<EmailFormValues> = {};
|
||||
if (!values.email) {
|
||||
error.email = 'Required';
|
||||
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
|
||||
error.email = 'Invalid email address';
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={sendEmail}
|
||||
validateOnMount={true}
|
||||
validate={validateEmail}
|
||||
>
|
||||
{({ isValid, isSubmitting }) => (
|
||||
<Form autoComplete='true' className='w-full h-full'>
|
||||
{
|
||||
disclosureLogin ?
|
||||
<div>
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button className="w-full text-left text-base font-light">
|
||||
<div className='flex items-center justify-between'>
|
||||
<p className='text-xl text-primary-text'>
|
||||
Sign in with email
|
||||
</p>
|
||||
<div className='bg-secondary-500 hover:bg-secondary-500 p-0.5 rounded-md duration-200 transition'>
|
||||
<ChevronDown
|
||||
className={`${open ? 'rotate-180 transform' : ''
|
||||
} h-5 text-secondary-text`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className='mt-2 text-left text-secondary-text'>
|
||||
Securely store your exchange accounts, access your full transfer history and more.
|
||||
</p>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel className="text-sm text-secondary-text font-normal mt-4">
|
||||
<div className='grid gap-4 grid-cols-5 items-center'>
|
||||
<div className="relative rounded-md shadow-sm col-span-3">
|
||||
<Field name="email">
|
||||
{({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
id='email'
|
||||
placeholder="[email protected]"
|
||||
autoComplete="email"
|
||||
type="email"
|
||||
className="h-12 pb-1 pt-0 text-primary-text focus:ring-primary focus:border-primary border-secondary-500 pr-42 block
|
||||
placeholder:text-secondary-text placeholder:text-sm placeholder:font-normal placeholder:opacity-50 bg-secondary-700 w-full font-semibold rounded-md"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
<div className='col-start-4 col-span-2'>
|
||||
<SubmitButton isDisabled={!isValid} isSubmitting={isSubmitting} >
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
:
|
||||
<Widget>
|
||||
<Widget.Content center={true}>
|
||||
<User className='w-16 h-16 text-primary self-center mt-auto' />
|
||||
<div>
|
||||
<p className='mb-6 mt-2 pt-2 text-2xl font-bold text-primary-text leading-6 text-center font-roboto'>
|
||||
What's your email?
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
<Field name="email">
|
||||
{({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
id='email'
|
||||
placeholder="[email protected]"
|
||||
autoComplete="email"
|
||||
type="email"
|
||||
className="h-12 pb-1 pt-0 text-primary-text focus:ring-primary focus:border-primary border-secondary-500 pr-42 block
|
||||
placeholder:text-secondary-text placeholder:text-sm placeholder:font-normal placeholder:opacity-50 bg-secondary-700 w-full font-semibold rounded-md"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
<div className='px-3 text-primary-text mt-6'>
|
||||
<p className='text-left text-sm mb-3 text-secondary-text font-semibold'>
|
||||
By signing in you get
|
||||
</p>
|
||||
<ul className='space-y-3'>
|
||||
<li className='flex gap-3'>
|
||||
<div>
|
||||
<ScrollText className='h-5 w-5 mt-0.5 text-primary' />
|
||||
</div>
|
||||
<div>
|
||||
<p className='font-semibold'>History</p>
|
||||
<p className='text-secondary-text text-sm'>
|
||||
Access your entire transaction history
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className='flex gap-3'>
|
||||
<div>
|
||||
<Mail className='h-5 w-5 mt-0.5 text-primary' />
|
||||
</div>
|
||||
<div>
|
||||
<p className='font-semibold'>Email updates</p>
|
||||
<p className='text-secondary-text text-sm'>
|
||||
Get a notification upon transfer completion
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className='flex gap-3'>
|
||||
<div>
|
||||
<Album className='h-5 w-5 mt-0.5 text-primary' />
|
||||
</div>
|
||||
<div>
|
||||
<p className='font-semibold'>Dedicated deposit address</p>
|
||||
<p className='text-secondary-text text-sm'>
|
||||
Get deposit addresses that stay the same and can be whitelisted in CEXes
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Widget.Content>
|
||||
<Widget.Footer>
|
||||
<SubmitButton isDisabled={!isValid} isSubmitting={isSubmitting} >
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</Widget.Footer>
|
||||
</Widget>
|
||||
}
|
||||
</Form>
|
||||
)}
|
||||
</Formik >
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendEmail;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { clusterApiUrl } from "@solana/web3.js";
|
||||
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
|
||||
import { PhantomWalletAdapter } from "@solana/wallet-adapter-phantom";
|
||||
import { CoinbaseWalletAdapter } from "@solana/wallet-adapter-coinbase";
|
||||
import { WalletConnectWalletAdapter } from "@solana/wallet-adapter-walletconnect";
|
||||
import { SolflareWalletAdapter } from "@solana/wallet-adapter-solflare";
|
||||
import { GlowWalletAdapter } from "@solana/wallet-adapter-glow";
|
||||
|
||||
import {
|
||||
ConnectionProvider,
|
||||
WalletProvider,
|
||||
} from "@solana/wallet-adapter-react";
|
||||
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
require("@solana/wallet-adapter-react-ui/styles.css");
|
||||
const WALLETCONNECT_PROJECT_ID = '28168903b2d30c75e5f7f2d71902581b';
|
||||
|
||||
function SolanaProvider({ children }: { children: ReactNode }) {
|
||||
const solNetwork = WalletAdapterNetwork.Mainnet;
|
||||
const endpoint = useMemo(() => clusterApiUrl(solNetwork), [solNetwork]);
|
||||
const wallets = useMemo(
|
||||
() => [
|
||||
new PhantomWalletAdapter(),
|
||||
new CoinbaseWalletAdapter(),
|
||||
new SolflareWalletAdapter(),
|
||||
new GlowWalletAdapter(),
|
||||
new WalletConnectWalletAdapter({
|
||||
network: solNetwork,
|
||||
options: {
|
||||
projectId: WALLETCONNECT_PROJECT_ID,
|
||||
},
|
||||
})
|
||||
],
|
||||
[solNetwork]
|
||||
);
|
||||
|
||||
return (
|
||||
<ConnectionProvider endpoint={endpoint}>
|
||||
<WalletProvider wallets={wallets} autoConnect={true}>
|
||||
<WalletModalProvider>
|
||||
{children}
|
||||
</WalletModalProvider>
|
||||
</WalletProvider>
|
||||
</ConnectionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default SolanaProvider;
|
||||
@@ -0,0 +1,351 @@
|
||||
import { Form, FormikErrors, useFormikContext } from "formik";
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import Image from 'next/image';
|
||||
import SwapButton from "../../buttons/swapButton";
|
||||
import React from "react";
|
||||
import NetworkFormField from "../../Input/NetworkFormField";
|
||||
import BridgeApiClient, { AddressBookItem } from "../../../lib/BridgeApiClient";
|
||||
import { SwapFormValues } from "../../DTOs/SwapFormValues";
|
||||
import { Partner } from "../../../Models/Partner";
|
||||
import Modal from "../../modal/modal";
|
||||
import { useSwapDataState, useSwapDataUpdate } from "../../../context/swap";
|
||||
import { useSettingsState } from "../../../context/settings";
|
||||
import { isValidAddress } from "../../../lib/addressValidator";
|
||||
import { CalculateMinAllowedAmount } from "../../../lib/fees";
|
||||
import shortenAddress from "../../utils/ShortenAddress";
|
||||
import useSWR from "swr";
|
||||
import { ApiResponse } from "../../../Models/ApiResponse";
|
||||
import { motion, useCycle } from "framer-motion";
|
||||
import ClickTooltip from "../../Tooltips/ClickTooltip";
|
||||
import ToggleButton from "../../buttons/toggleButton";
|
||||
import { ArrowUpDown, Fuel } from 'lucide-react'
|
||||
import { useAuthState } from "../../../context/authContext";
|
||||
import WarningMessage from "../../WarningMessage";
|
||||
import { FilterDestinationLayers, FilterSourceLayers, GetDefaultNetwork, GetNetworkCurrency } from "../../../helpers/settingsHelper";
|
||||
import KnownInternalNames from "../../../lib/knownIds";
|
||||
import { Widget } from "../../Widget/Index";
|
||||
import { classNames } from "../../utils/classNames";
|
||||
import GasDetails from "../../gasDetails";
|
||||
import { useQueryState } from "../../../context/query";
|
||||
import FeeDetails from "../../DisclosureComponents/FeeDetails";
|
||||
import AmountField from "../../Input/Amount"
|
||||
import { Balance, Gas } from "../../../Models/Balance";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type Props = {
|
||||
isPartnerWallet?: boolean,
|
||||
partner?: Partner,
|
||||
}
|
||||
|
||||
const ReserveGasNote = dynamic(() => import("../../ReserveGasNote"), {
|
||||
loading: () => <></>,
|
||||
});
|
||||
|
||||
const Address = dynamic(() => import("../../Input/Address"), {
|
||||
loading: () => <></>,
|
||||
});
|
||||
|
||||
|
||||
const SwapForm: FC<Props> = ({ partner, isPartnerWallet }) => {
|
||||
const {
|
||||
values,
|
||||
setValues,
|
||||
errors, isValid, isSubmitting, setFieldValue
|
||||
} = useFormikContext<SwapFormValues>();
|
||||
|
||||
const { to: destination } = values
|
||||
const settings = useSettingsState();
|
||||
const source = values.from
|
||||
const asset = values.currency?.asset
|
||||
const { authData } = useAuthState()
|
||||
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
const address_book_endpoint = authData?.access_token ? `/address_book/recent` : null
|
||||
const { data: address_book } = useSWR<ApiResponse<AddressBookItem[]>>(address_book_endpoint, bridgeApiClient.fetcher, { dedupingInterval: 60000 })
|
||||
|
||||
const minAllowedAmount = CalculateMinAllowedAmount(values, settings.networks, settings.currencies);
|
||||
const partnerImage = partner?.logo_url
|
||||
const { setDepositeAddressIsfromAccount, setAddressConfirmed } = useSwapDataUpdate()
|
||||
const { depositeAddressIsfromAccount } = useSwapDataState()
|
||||
const query = useQueryState();
|
||||
const [valuesSwapperDisabled, setValuesSwapperDisabled] = useState(false)
|
||||
const [showAddressModal, setShowAddressModal] = useState(false);
|
||||
const lockAddress =
|
||||
(values.destination_address && values.to)
|
||||
&& isValidAddress(values.destination_address, values.to)
|
||||
&& (((query.lockAddress || query.hideAddress) && (query.appName !== "imxMarketplace" || settings.validSignatureisPresent)));
|
||||
|
||||
const actionDisplayName = query?.actionButtonText || "Swap now"
|
||||
|
||||
const handleConfirmToggleChange = (value: boolean) => {
|
||||
setFieldValue('refuel', value)
|
||||
}
|
||||
const depositeAddressIsfromAccountRef = useRef<boolean | null>(depositeAddressIsfromAccount);
|
||||
|
||||
useEffect(() => {
|
||||
depositeAddressIsfromAccountRef.current = depositeAddressIsfromAccount
|
||||
return () => { (depositeAddressIsfromAccountRef.current = null); return }
|
||||
}, [depositeAddressIsfromAccount])
|
||||
|
||||
useEffect(() => {
|
||||
if (!destination?.isExchange && (!source || !asset || !GetNetworkCurrency(source, asset)?.is_refuel_enabled)) {
|
||||
handleConfirmToggleChange(false)
|
||||
}
|
||||
}, [asset, destination, source])
|
||||
|
||||
useEffect(() => {
|
||||
setAddressConfirmed(false)
|
||||
}, [source])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
(await import("../../Input/Address")).default
|
||||
})()
|
||||
}, [destination])
|
||||
|
||||
useEffect(() => {
|
||||
if (!destination?.isExchange && values.refuel && values.amount && Number(values.amount) < minAllowedAmount) {
|
||||
setFieldValue('amount', minAllowedAmount)
|
||||
}
|
||||
}, [values.refuel, destination])
|
||||
|
||||
const previouslySelectedDestination = useRef(destination);
|
||||
|
||||
//If destination changed to exchange, remove destination_address
|
||||
useEffect(() => {
|
||||
if ((previouslySelectedDestination.current && destination?.isExchange != previouslySelectedDestination.current?.isExchange
|
||||
|| (destination?.isExchange && previouslySelectedDestination.current?.isExchange && destination?.internal_name != previouslySelectedDestination.current?.internal_name)
|
||||
|| destination && !isValidAddress(values.destination_address, destination)) && !lockAddress) {
|
||||
setFieldValue("destination_address", '')
|
||||
setDepositeAddressIsfromAccount(false)
|
||||
}
|
||||
previouslySelectedDestination.current = destination
|
||||
}, [destination])
|
||||
|
||||
useEffect(() => {
|
||||
if (!destination?.isExchange && values.refuel && Number(values.amount) < minAllowedAmount) {
|
||||
setFieldValue('amount', minAllowedAmount)
|
||||
}
|
||||
}, [values.refuel, destination])
|
||||
|
||||
const valuesSwapper = useCallback(() => {
|
||||
setValues({ ...values, from: values.to, to: values.from }, true)
|
||||
}, [values])
|
||||
|
||||
const [animate, cycle] = useCycle(
|
||||
{ rotate: 0 },
|
||||
{ rotate: 180 }
|
||||
);
|
||||
|
||||
const lockedCurrency = query?.lockAsset ? settings.currencies?.find(c => c?.asset?.toUpperCase() === asset?.toUpperCase()) : null
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const filteredSourceLayers = FilterSourceLayers(settings.layers, source, lockedCurrency);
|
||||
const filteredDestinationLayers = FilterDestinationLayers(settings.layers, destination, lockedCurrency);
|
||||
|
||||
const sourceCanBeSwapped = filteredDestinationLayers.some(l => l.internal_name === source?.internal_name)
|
||||
const destinationCanBeSwapped = filteredSourceLayers.some(l => l.internal_name === destination?.internal_name)
|
||||
|
||||
if (query.lockTo || query.lockFrom || query.hideTo || query.hideFrom) {
|
||||
setValuesSwapperDisabled(true)
|
||||
return;
|
||||
}
|
||||
if (!(sourceCanBeSwapped || destinationCanBeSwapped)) {
|
||||
setValuesSwapperDisabled(true)
|
||||
return;
|
||||
}
|
||||
setValuesSwapperDisabled(false)
|
||||
|
||||
}, [source, destination, query, settings, lockedCurrency])
|
||||
|
||||
const destinationNetwork = GetDefaultNetwork(destination, values?.currency?.asset)
|
||||
const destination_native_currency = !destination?.isExchange && destinationNetwork?.native_currency
|
||||
|
||||
const averageTimeString = (values?.to?.isExchange === true ?
|
||||
values?.to?.assets?.find(a => a?.asset === values?.currency?.asset && a?.is_default)?.network?.average_completion_time
|
||||
: values?.to?.average_completion_time)
|
||||
|| ''
|
||||
const parts = averageTimeString?.split(":");
|
||||
const averageTimeInMinutes = parts && parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10) + parseInt(parts[2]) / 60
|
||||
|
||||
const hideAddress = query?.hideAddress
|
||||
&& query?.to
|
||||
&& query?.destAddress
|
||||
&& (query?.lockTo || query?.hideTo)
|
||||
&& isValidAddress(query?.destAddress as string, destination)
|
||||
|
||||
const handleReserveGas = useCallback((walletBalance: Balance, networkGas: Gas) => {
|
||||
if (walletBalance && networkGas)
|
||||
setFieldValue('amount', walletBalance?.amount - networkGas?.gas)
|
||||
}, [values.amount])
|
||||
|
||||
return <>
|
||||
<Widget className="sm:min-h-[504px]">
|
||||
<Form className={`h-full ${(isSubmitting) ? 'pointer-events-none' : 'pointer-events-auto'}`} >
|
||||
<Widget.Content>
|
||||
<div className='flex-col relative flex justify-between w-full space-y-4 mb-3.5 leading-4'>
|
||||
{!(query?.hideFrom && values?.from) && <div className="flex flex-col w-full">
|
||||
<NetworkFormField direction="from" label="From" />
|
||||
</div>}
|
||||
{!query?.hideFrom && !query?.hideTo &&
|
||||
<button type="button"
|
||||
aria-label="Reverse the source and destination"
|
||||
disabled={valuesSwapperDisabled}
|
||||
onClick={valuesSwapper}
|
||||
className='absolute right-[calc(50%-16px)] top-[74px] z-10 border-4 border-secondary-900 bg-secondary-900 rounded-full disabled:cursor-not-allowed hover:text-primary disabled:text-secondary-text duration-200 transition'>
|
||||
<motion.div
|
||||
animate={animate}
|
||||
transition={{ duration: 0.3 }}
|
||||
onTap={() => !valuesSwapperDisabled && cycle()}
|
||||
>
|
||||
<ArrowUpDown className={classNames(valuesSwapperDisabled && 'opacity-50', "w-8 h-auto p-1 bg-secondary-900 border-2 border-secondary-500 rounded-full disabled:opacity-30")} />
|
||||
</motion.div>
|
||||
</button>}
|
||||
{!(query?.hideTo && values?.to) && <div className="flex flex-col w-full">
|
||||
<NetworkFormField direction="to" label="To" />
|
||||
</div>}
|
||||
</div>
|
||||
<div className="mb-6 leading-4">
|
||||
<AmountField />
|
||||
</div>
|
||||
{
|
||||
!hideAddress ?
|
||||
<div className="w-full mb-3.5 leading-4">
|
||||
<label htmlFor="destination_address" className="block font-semibold text-secondary-text text-sm">
|
||||
{`To ${values?.to?.display_name || ''} address`}
|
||||
</label>
|
||||
<AddressButton
|
||||
disabled={!values.to || !values.from}
|
||||
isPartnerWallet={!!isPartnerWallet}
|
||||
openAddressModal={() => setShowAddressModal(true)}
|
||||
partnerImage={partnerImage}
|
||||
values={values} />
|
||||
<Modal
|
||||
header={`To ${values?.to?.display_name || ''} address`}
|
||||
height="fit"
|
||||
show={showAddressModal} setShow={setShowAddressModal}
|
||||
className="min-h-[70%]"
|
||||
>
|
||||
<Address
|
||||
close={() => setShowAddressModal(false)}
|
||||
disabled={lockAddress || (!values.to || !values.from)}
|
||||
name={"destination_address"}
|
||||
partnerImage={partnerImage}
|
||||
isPartnerWallet={!!isPartnerWallet}
|
||||
partner={partner}
|
||||
address_book={address_book?.data}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
<div className="w-full">
|
||||
{
|
||||
destination && asset && !destination.isExchange && GetNetworkCurrency(destination, asset)?.is_refuel_enabled && !query?.hideRefuel &&
|
||||
<div className="flex items-center justify-between px-3.5 py-3 bg-secondary-700 border border-secondary-500 rounded-lg mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Fuel className='h-8 w-8 text-primary' />
|
||||
<div>
|
||||
<p className="font-medium flex items-center">
|
||||
<span>Need gas?</span>
|
||||
<ClickTooltip text={`You will get a small amount of ${destination_native_currency} that you can use to pay for gas fees.`} />
|
||||
</p>
|
||||
<p className="font-light text-xs">
|
||||
<span>Get </span><span className="font-semibold">{destination_native_currency}</span><span> to pay fees in </span><span>{values.to?.display_name}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ToggleButton name="refuel" value={!!values?.refuel} onChange={handleConfirmToggleChange} />
|
||||
</div>
|
||||
}
|
||||
<FeeDetails values={values} />
|
||||
{
|
||||
//TODO refactor
|
||||
destination && asset && GetNetworkCurrency(destination, asset)?.status == 'insufficient_liquidity' &&
|
||||
<WarningMessage messageType="warning" className="mt-4">
|
||||
<span className="font-normal"><span>We're experiencing delays for transfers of</span> <span>{values?.currency?.asset}</span> <span>to</span> <span>{values?.to?.display_name}</span><span>. Estimated arrival time can take up to 2 hours.</span></span>
|
||||
</WarningMessage>
|
||||
}
|
||||
{
|
||||
destination && asset && GetNetworkCurrency(destination, asset)?.status !== 'insufficient_liquidity' && destination?.internal_name === KnownInternalNames.Networks.StarkNetMainnet && averageTimeInMinutes > 30 &&
|
||||
<WarningMessage messageType="warning" className="mt-4">
|
||||
<span className="font-normal"><span>{destination?.display_name}</span> <span>network congestion. Transactions can take up to 1 hour.</span></span>
|
||||
</WarningMessage>
|
||||
}
|
||||
{
|
||||
values.amount &&
|
||||
<ReserveGasNote onSubmit={(walletBalance, networkGas) => handleReserveGas(walletBalance, networkGas)} />
|
||||
}
|
||||
</div>
|
||||
</Widget.Content>
|
||||
<Widget.Footer>
|
||||
<SwapButton
|
||||
className="plausible-event-name=Swap+initiated"
|
||||
type='submit'
|
||||
isDisabled={!isValid}
|
||||
isSubmitting={isSubmitting}>
|
||||
{ActionText(errors, actionDisplayName as string)}
|
||||
</SwapButton>
|
||||
</Widget.Footer>
|
||||
</Form >
|
||||
</Widget>
|
||||
{
|
||||
process.env.NEXT_PUBLIC_SHOW_GAS_DETAILS === 'true'
|
||||
&& values.from
|
||||
&& values.currency &&
|
||||
<GasDetails network={values.from} currency={values.currency} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function ActionText(errors: FormikErrors<SwapFormValues>, actionDisplayName: string): string {
|
||||
return errors.from?.toString()
|
||||
|| errors.to?.toString()
|
||||
|| errors.amount
|
||||
|| errors.destination_address
|
||||
|| (actionDisplayName)
|
||||
}
|
||||
|
||||
const TruncatedAdrress = ({ address }: { address: string }) => {
|
||||
const shortAddress = shortenAddress(address)
|
||||
return <div className="tracking-wider text-primary-text">{shortAddress}</div>
|
||||
}
|
||||
|
||||
type AddressButtonProps = {
|
||||
openAddressModal: () => void;
|
||||
isPartnerWallet: boolean;
|
||||
values: SwapFormValues;
|
||||
partnerImage?: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
const AddressButton: FC<AddressButtonProps> = ({ openAddressModal, isPartnerWallet, values, partnerImage, disabled }) => {
|
||||
const destination = values?.to
|
||||
return <button type="button" disabled={disabled} onClick={openAddressModal} className="flex rounded-lg space-x-3 items-center cursor-pointer shadow-sm mt-1.5 text-primary-text-placeholder bg-secondary-700 border-secondary-500 border disabled:cursor-not-allowed h-12 leading-4 focus:ring-primary focus:border-primary font-semibold w-full px-3.5 py-3">
|
||||
{isPartnerWallet && !destination?.isExchange &&
|
||||
<div className="shrink-0 flex items-center pointer-events-none">
|
||||
{
|
||||
partnerImage &&
|
||||
<Image
|
||||
alt="Partner logo"
|
||||
className='rounded-md object-contain'
|
||||
src={partnerImage}
|
||||
width="24"
|
||||
height="24"></Image>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div className="truncate">
|
||||
{values.destination_address ?
|
||||
<TruncatedAdrress address={values.destination_address} />
|
||||
:
|
||||
<span>Enter your address here</span>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default SwapForm
|
||||
@@ -0,0 +1,250 @@
|
||||
import { Formik, FormikProps } from "formik";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useSettingsState } from "../../../context/settings";
|
||||
import { SwapFormValues } from "../../DTOs/SwapFormValues";
|
||||
import { useSwapDataState, useSwapDataUpdate } from "../../../context/swap";
|
||||
import React from "react";
|
||||
import ConnectNetwork from "../../ConnectNetwork";
|
||||
import toast from "react-hot-toast";
|
||||
import MainStepValidation from "../../../lib/mainStepValidator";
|
||||
import { generateSwapInitialValues, generateSwapInitialValuesFromSwap } from "../../../lib/generateSwapInitialValues";
|
||||
import BridgeApiClient from "../../../lib/BridgeApiClient";
|
||||
import Modal from "../../modal/modal";
|
||||
import SwapForm from "./Form";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { ApiResponse } from "../../../Models/ApiResponse";
|
||||
import { Partner } from "../../../Models/Partner";
|
||||
import { UserType, useAuthDataUpdate } from "../../../context/authContext";
|
||||
import { ApiError, LSAPIKnownErrorCode } from "../../../Models/ApiError";
|
||||
import { resolvePersistantQueryParams } from "../../../helpers/querryHelper";
|
||||
import { useQueryState } from "../../../context/query";
|
||||
import TokenService from "../../../lib/TokenService";
|
||||
import BridgeAuthApiClient from "../../../lib/userAuthApiClient";
|
||||
import StatusIcon from "../../SwapHistory/StatusIcons";
|
||||
import Image from 'next/image';
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import dynamic from "next/dynamic";
|
||||
import ResizablePanel from "../../ResizablePanel";
|
||||
|
||||
type NetworkToConnect = {
|
||||
DisplayName: string;
|
||||
AppURL: string;
|
||||
}
|
||||
const SwapDetails = dynamic(() => import(".."), {
|
||||
loading: () => <div className="w-full h-[450px]">
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="flex-1 space-y-6 py-1">
|
||||
<div className="h-32 bg-secondary-700 rounded-lg"></div>
|
||||
<div className="h-40 bg-secondary-700 rounded-lg"></div>
|
||||
<div className="h-12 bg-secondary-700 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
|
||||
export default function Form() {
|
||||
const formikRef = useRef<FormikProps<SwapFormValues>>(null);
|
||||
const [showConnectNetworkModal, setShowConnectNetworkModal] = useState(false);
|
||||
const [showSwapModal, setShowSwapModal] = useState(false);
|
||||
const [networkToConnect, setNetworkToConnect] = useState<NetworkToConnect>();
|
||||
const router = useRouter();
|
||||
const { updateAuthData, setUserType } = useAuthDataUpdate()
|
||||
|
||||
const settings = useSettingsState();
|
||||
const query = useQueryState()
|
||||
const { createSwap, setSwapId } = useSwapDataUpdate()
|
||||
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
const { data: partnerData } = useSWR<ApiResponse<Partner>>(query?.appName && `/apps?name=${query?.appName}`, bridgeApiClient.fetcher)
|
||||
const partner = query?.appName && partnerData?.data?.name?.toLowerCase() === (query?.appName as string)?.toLowerCase() ? partnerData?.data : undefined
|
||||
|
||||
const { swap } = useSwapDataState()
|
||||
|
||||
useEffect(() => {
|
||||
if (swap) {
|
||||
const initialValues = generateSwapInitialValuesFromSwap(swap, settings)
|
||||
formikRef?.current?.resetForm({ values: initialValues })
|
||||
formikRef?.current?.validateForm(initialValues)
|
||||
}
|
||||
}, [swap])
|
||||
|
||||
const handleSubmit = useCallback(async (values: SwapFormValues) => {
|
||||
try {
|
||||
const accessToken = TokenService.getAuthData()?.access_token
|
||||
if (!accessToken) {
|
||||
try {
|
||||
var apiClient = new BridgeAuthApiClient();
|
||||
const res = await apiClient.guestConnectAsync()
|
||||
updateAuthData(res)
|
||||
setUserType(UserType.GuestUser)
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(error.response?.data?.error || error.message)
|
||||
return;
|
||||
}
|
||||
}
|
||||
const swapId = await createSwap(values, query, partner);
|
||||
|
||||
if (swapId) {
|
||||
setSwapId(swapId)
|
||||
var swapURL = window.location.protocol + "//"
|
||||
+ window.location.host + `/swap/${swapId}`;
|
||||
const params = resolvePersistantQueryParams(router.query)
|
||||
if (params && Object.keys(params).length) {
|
||||
const search = new URLSearchParams(params as any);
|
||||
if (search)
|
||||
swapURL += `?${search}`
|
||||
}
|
||||
window.history.replaceState({ ...window.history.state, as: swapURL, url: swapURL }, '', swapURL);
|
||||
setShowSwapModal(true)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const data: ApiError = error?.response?.data?.error
|
||||
if (data?.code === LSAPIKnownErrorCode.BLACKLISTED_ADDRESS) {
|
||||
toast.error("You can't transfer to that address. Please double check.")
|
||||
}
|
||||
else if (data?.code === LSAPIKnownErrorCode.INVALID_ADDRESS_ERROR) {
|
||||
toast.error(`Enter a valid ${values.to?.display_name} address`)
|
||||
}
|
||||
else if (data?.code === LSAPIKnownErrorCode.UNACTIVATED_ADDRESS_ERROR && values.to) {
|
||||
setNetworkToConnect({
|
||||
DisplayName: values.to?.display_name,
|
||||
AppURL: data.message
|
||||
})
|
||||
setShowConnectNetworkModal(true);
|
||||
}
|
||||
else {
|
||||
toast.error(error.message)
|
||||
}
|
||||
}
|
||||
}, [createSwap, query, partner, router, updateAuthData, setUserType, swap])
|
||||
|
||||
const destAddress: string = query?.destAddress as string;
|
||||
|
||||
const isPartnerAddress = partner && destAddress;
|
||||
|
||||
const isPartnerWallet = isPartnerAddress && partner?.is_wallet;
|
||||
|
||||
const initialValues: SwapFormValues = swap ? generateSwapInitialValuesFromSwap(swap, settings)
|
||||
: generateSwapInitialValues(settings, query)
|
||||
const initiallyValidation = MainStepValidation({ settings, query })(initialValues)
|
||||
const initiallyInValid = Object.values(initiallyValidation)?.filter(v => v).length > 0
|
||||
|
||||
return <>
|
||||
<div className="rounded-r-lg cursor-pointer absolute z-10 md:mt-3 border-l-0">
|
||||
<AnimatePresence mode='wait'>
|
||||
{
|
||||
swap &&
|
||||
!showSwapModal &&
|
||||
<PendingSwap onClick={() => setShowSwapModal(true)} />
|
||||
}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<Modal height="fit" show={showConnectNetworkModal} setShow={setShowConnectNetworkModal} header={`${networkToConnect?.DisplayName} connect`}>
|
||||
{networkToConnect && <ConnectNetwork NetworkDisplayName={networkToConnect?.DisplayName} AppURL={networkToConnect?.AppURL} />}
|
||||
</Modal>
|
||||
<Modal height='fit' show={showSwapModal} setShow={setShowSwapModal} header={`Complete the swap`}>
|
||||
<ResizablePanel>
|
||||
<SwapDetails type="contained" />
|
||||
</ResizablePanel>
|
||||
</Modal>
|
||||
<Formik
|
||||
innerRef={formikRef}
|
||||
initialValues={initialValues}
|
||||
validateOnMount={true}
|
||||
validate={MainStepValidation({ settings, query })}
|
||||
onSubmit={handleSubmit}
|
||||
isInitialValid={!initiallyInValid}
|
||||
>
|
||||
<SwapForm isPartnerWallet={!!isPartnerWallet} partner={partner} />
|
||||
</Formik>
|
||||
</>
|
||||
}
|
||||
const textMotion = {
|
||||
rest: {
|
||||
color: "grey",
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
type: "tween",
|
||||
ease: "easeIn"
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
color: "blue",
|
||||
x: 30,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
type: "tween",
|
||||
ease: "easeOut"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const PendingSwap = ({ onClick }: { onClick: () => void }) => {
|
||||
const { swap } = useSwapDataState()
|
||||
const { source_exchange: source_exchange_internal_name,
|
||||
destination_network: destination_network_internal_name,
|
||||
source_network: source_network_internal_name,
|
||||
destination_exchange: destination_exchange_internal_name,
|
||||
} = swap || {}
|
||||
|
||||
const settings = useSettingsState()
|
||||
|
||||
if (!swap)
|
||||
return <></>
|
||||
|
||||
const { exchanges, networks, resolveImgSrc } = settings
|
||||
const source = source_exchange_internal_name ? exchanges.find(e => e.internal_name === source_exchange_internal_name) : networks.find(e => e.internal_name === source_network_internal_name)
|
||||
const destination_exchange = destination_exchange_internal_name && exchanges.find(e => e.internal_name === destination_exchange_internal_name)
|
||||
const destination = destination_exchange_internal_name ? destination_exchange : networks.find(n => n.internal_name === destination_network_internal_name)
|
||||
|
||||
return <motion.div
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -10, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
onClick={onClick}
|
||||
initial="rest" whileHover="hover" animate="rest"
|
||||
className="relative bg-secondary-600 rounded-r-lg">
|
||||
<motion.div
|
||||
variants={textMotion}
|
||||
className="flex items-center bg-secondary-600 rounded-r-lg">
|
||||
<div className="text-primary-text flex px-3 p-2 items-center space-x-2">
|
||||
<span className="flex items-center">
|
||||
{swap && <StatusIcon swap={swap} short={true} />}
|
||||
</span>
|
||||
<div className="flex-shrink-0 h-5 w-5 relative">
|
||||
{source &&
|
||||
<Image
|
||||
src={resolveImgSrc(source)}
|
||||
alt="From Logo"
|
||||
height="60"
|
||||
width="60"
|
||||
className="rounded-md object-contain"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<ChevronRight className="block h-4 w-4 mx-1" />
|
||||
<div className="flex-shrink-0 h-5 w-5 relative block">
|
||||
{destination &&
|
||||
<Image
|
||||
src={resolveImgSrc(destination)}
|
||||
alt="To Logo"
|
||||
height="60"
|
||||
width="60"
|
||||
className="rounded-md object-contain"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { FC, useCallback, useEffect } from "react";
|
||||
import MessageComponent from "../MessageComponent";
|
||||
import SubmitButton, { DoubleLineText } from "../buttons/submitButton";
|
||||
import GoHomeButton from "../utils/GoHome";
|
||||
import { useAuthState } from "../../context/authContext";
|
||||
import { useIntercom } from "react-use-intercom";
|
||||
import { TrackEvent } from '../../pages/_document';
|
||||
import { Home, MessageSquare } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const NotFound: FC = () => {
|
||||
|
||||
const { email, userId } = useAuthState()
|
||||
const { boot, show, update } = useIntercom()
|
||||
const { query } = useRouter()
|
||||
const updateWithProps = () => update({ email: email, userId: userId, customAttributes: { swapId: query?.swapId } })
|
||||
|
||||
useEffect(() => {
|
||||
plausible(TrackEvent.SwapFailed)
|
||||
}, [])
|
||||
|
||||
const startIntercom = useCallback(() => {
|
||||
boot();
|
||||
show();
|
||||
updateWithProps()
|
||||
}, [boot, show, updateWithProps])
|
||||
|
||||
return <MessageComponent>
|
||||
<MessageComponent.Content icon='red'>
|
||||
<MessageComponent.Header>
|
||||
Swap not found
|
||||
</MessageComponent.Header>
|
||||
<MessageComponent.Description>
|
||||
<span className="text-sm mt-5">
|
||||
<p>
|
||||
Your funds are safe, but there seems to be an issue with the swap.
|
||||
</p>
|
||||
<p>
|
||||
Please contact our support team and we’ll help you fix this.
|
||||
</p>
|
||||
</span>
|
||||
</MessageComponent.Description>
|
||||
</MessageComponent.Content>
|
||||
<MessageComponent.Buttons>
|
||||
<MessageComponent.Buttons>
|
||||
<div className="flex flex-row text-primary-text text-base space-x-2">
|
||||
<div className='basis-1/3'>
|
||||
<SubmitButton text_align='left' onClick={startIntercom} isDisabled={false} isSubmitting={false} buttonStyle='outline' icon={<MessageSquare className="h-5 w-5" aria-hidden="true" />}>
|
||||
<DoubleLineText
|
||||
colorStyle='mltln-text-dark'
|
||||
primaryText='Support'
|
||||
secondarytext='Contact'
|
||||
/>
|
||||
</SubmitButton>
|
||||
</div>
|
||||
<div className='basis-2/3'>
|
||||
<GoHomeButton>
|
||||
<SubmitButton button_align='right' text_align='left' isDisabled={false} isSubmitting={false} buttonStyle='outline' icon={<Home className="h-5 w-5" aria-hidden="true" />}>
|
||||
<DoubleLineText
|
||||
colorStyle='mltln-text-dark'
|
||||
primaryText='Swap'
|
||||
secondarytext='Do another'
|
||||
/>
|
||||
</SubmitButton>
|
||||
</GoHomeButton>
|
||||
</div>
|
||||
</div>
|
||||
</MessageComponent.Buttons>
|
||||
</MessageComponent.Buttons>
|
||||
</MessageComponent>
|
||||
}
|
||||
export default NotFound
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Check, X } from "lucide-react";
|
||||
import { classNames } from "../utils/classNames";
|
||||
import { Gauge } from "../gauge";
|
||||
import { ProgressStatus, StatusStep } from "./Withdraw/Processing/types";
|
||||
|
||||
function renderStepIcon(step: StatusStep) {
|
||||
switch (step.status) {
|
||||
case ProgressStatus.Complete:
|
||||
return (
|
||||
<span className="relative z-10 flex h-8 w-8 items-center justify-center rounded-full bg-primary/20">
|
||||
<Check className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
|
||||
case ProgressStatus.Current:
|
||||
return (
|
||||
<span className="animate-spin">
|
||||
<Gauge value={40} size="verySmall" />
|
||||
</span>
|
||||
);
|
||||
|
||||
case ProgressStatus.Failed:
|
||||
return (
|
||||
<span className="relative z-10 flex h-8 w-8 items-center justify-center rounded-full bg-primary/20">
|
||||
<X className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
case ProgressStatus.Delayed:
|
||||
return (
|
||||
<span className="animate-spin opacity-50">
|
||||
<Gauge value={40} size="verySmall" />
|
||||
</span>)
|
||||
|
||||
default:
|
||||
return (
|
||||
<span className="relative z-10 flex h-8 w-8 items-center justify-center rounded-full border-2 border-primary/20">
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Step({ step, isLastStep }: { step: StatusStep, isLastStep: boolean }) {
|
||||
return (
|
||||
<li className={classNames(isLastStep ? '' : 'pb-10', 'relative')} key={step?.name}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
{!isLastStep && (
|
||||
<div className={`absolute top-1/2 left-4 -ml-px mt-0.5 h-[40%] w-0.5 ${step.status === "complete" ? "bg-primary" : "bg-primary/20"} `} aria-hidden="true" />
|
||||
)}
|
||||
<div className={`group relative flex ${step?.description ? "items-start" : "items-center"}`}>
|
||||
<span className="flex h-9 items-center text-primary-text" aria-hidden="true">
|
||||
{renderStepIcon(step)}
|
||||
</span>
|
||||
<span className="ml-3 flex min-w-0 flex-col">
|
||||
<span className={`text-sm font-medium ${step.status === "current" ? "text-primary" : step.status === "upcoming" ? "text-secondary-text/70" : "text-primary-text"}`}>{step.name}</span>
|
||||
{step?.description &&
|
||||
<span className="text-sm text-secondary-text">{step?.description}</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default Step;
|
||||
@@ -0,0 +1,14 @@
|
||||
import Step from "./Step";
|
||||
import { StatusStep } from "./Withdraw/Processing/types";
|
||||
|
||||
export default function Steps({ steps }: { steps: StatusStep[] }) {
|
||||
return (
|
||||
<nav aria-label="Progress">
|
||||
<ol role="list" className="overflow-hidden">
|
||||
{steps.map((step, index) => (
|
||||
<Step key={index} step={step} isLastStep={index === steps.length - 1} />
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import Image from "next/image";
|
||||
import { Fuel } from "lucide-react";
|
||||
import { FC, useMemo } from "react";
|
||||
import { Currency } from "../../../Models/Currency";
|
||||
import { Layer } from "../../../Models/Layer";
|
||||
import { useSettingsState } from "../../../context/settings";
|
||||
import { truncateDecimals } from "../../utils/RoundDecimals";
|
||||
import shortenAddress, { shortenEmail } from "../../utils/ShortenAddress";
|
||||
import BridgeApiClient from "../../../lib/BridgeApiClient";
|
||||
import { ApiResponse } from "../../../Models/ApiResponse";
|
||||
import { Partner } from "../../../Models/Partner";
|
||||
import useSWR from 'swr'
|
||||
import KnownInternalNames from "../../../lib/knownIds";
|
||||
import useWallet from "../../../hooks/useWallet";
|
||||
import { useQueryState } from "../../../context/query";
|
||||
import { useSwapDataState } from "../../../context/swap";
|
||||
|
||||
type SwapInfoProps = {
|
||||
currency: Currency,
|
||||
source: Layer,
|
||||
destination: Layer;
|
||||
requestedAmount: number;
|
||||
receiveAmount?: number;
|
||||
destinationAddress: string;
|
||||
hasRefuel?: boolean;
|
||||
refuelAmount?: number;
|
||||
fee?: number,
|
||||
exchange_account_connected: boolean;
|
||||
exchange_account_name?: string;
|
||||
}
|
||||
|
||||
const Summary: FC<SwapInfoProps> = ({ currency, source: from, destination: to, requestedAmount, receiveAmount, destinationAddress, hasRefuel, refuelAmount, fee, exchange_account_connected, exchange_account_name }) => {
|
||||
const { resolveImgSrc, currencies, networks } = useSettingsState()
|
||||
const { getWithdrawalProvider: getProvider } = useWallet()
|
||||
const provider = useMemo(() => {
|
||||
return from && getProvider(from)
|
||||
}, [from, getProvider])
|
||||
|
||||
const wallet = provider?.getConnectedWallet()
|
||||
|
||||
const { selectedAssetNetwork } = useSwapDataState()
|
||||
|
||||
const {
|
||||
hideFrom,
|
||||
hideTo,
|
||||
account,
|
||||
appName,
|
||||
hideAddress
|
||||
} = useQueryState()
|
||||
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
const { data: partnerData } = useSWR<ApiResponse<Partner>>(appName && `/apps?name=${appName}`, bridgeApiClient.fetcher)
|
||||
const partner = partnerData?.data
|
||||
|
||||
const source = hideFrom ? partner : from
|
||||
const destination = hideTo ? partner : to
|
||||
|
||||
const requestedAmountInUsd = (currency?.usd_price * requestedAmount).toFixed(2)
|
||||
const receiveAmountInUsd = receiveAmount ? (currency?.usd_price * receiveAmount).toFixed(2) : undefined
|
||||
const nativeCurrency = refuelAmount && to?.isExchange === false ?
|
||||
currencies.find(c => c.asset === to?.native_currency) : null
|
||||
|
||||
const truncatedRefuelAmount = (hasRefuel && refuelAmount) ?
|
||||
truncateDecimals(refuelAmount, nativeCurrency?.precision) : null
|
||||
const refuelAmountInUsd = ((nativeCurrency?.usd_price || 1) * (truncatedRefuelAmount || 0)).toFixed(2)
|
||||
|
||||
let sourceAccountAddress = ""
|
||||
if (hideFrom && account) {
|
||||
sourceAccountAddress = shortenAddress(account);
|
||||
}
|
||||
else if (wallet && !from?.isExchange) {
|
||||
sourceAccountAddress = shortenAddress(wallet.address);
|
||||
}
|
||||
else if (from?.internal_name === KnownInternalNames.Exchanges.Coinbase && exchange_account_connected) {
|
||||
sourceAccountAddress = shortenEmail(exchange_account_name, 10);
|
||||
}
|
||||
else if (from?.isExchange) {
|
||||
sourceAccountAddress = "Exchange"
|
||||
}
|
||||
else {
|
||||
sourceAccountAddress = "Network"
|
||||
}
|
||||
|
||||
const destAddress = (hideAddress && hideTo && account) ? account : destinationAddress
|
||||
const sourceCurrencyName = selectedAssetNetwork?.network?.currencies.find(c => c.asset === currency.asset)?.name || currency?.asset
|
||||
const destCurrencyName = networks?.find(n => n.internal_name === to?.internal_name)?.currencies?.find(c => c?.asset === currency?.asset)?.name || currency?.asset
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-normal flex flex-col w-full relative z-10 space-y-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-3">
|
||||
{source && <Image src={resolveImgSrc(source)} alt={source.display_name} width={32} height={32} className="rounded-full" />}
|
||||
<div>
|
||||
<p className="text-primary-text text-sm leading-5">{source?.display_name}</p>
|
||||
{
|
||||
sourceAccountAddress &&
|
||||
<p className="text-sm text-secondary-text">{sourceAccountAddress}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-primary-text text-sm">{truncateDecimals(requestedAmount, currency.precision)} {sourceCurrencyName}</p>
|
||||
<p className="text-secondary-text text-sm flex justify-end">${requestedAmountInUsd}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full ">
|
||||
<div className="flex items-center gap-3">
|
||||
{destination && <Image src={resolveImgSrc(destination)} alt={destination.display_name} width={32} height={32} className="rounded-full" />}
|
||||
<div>
|
||||
<p className="text-primary-text text-sm leading-5">{destination?.display_name}</p>
|
||||
<p className="text-sm text-secondary-text">{shortenAddress(destAddress as string)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
fee != undefined && receiveAmount != undefined && fee >= 0 ?
|
||||
<div className="flex flex-col justify-end">
|
||||
<p className="text-primary-text text-sm">{truncateDecimals(receiveAmount, currency.precision)} {destCurrencyName}</p>
|
||||
<p className="text-secondary-text text-sm flex justify-end">${receiveAmountInUsd}</p>
|
||||
</div>
|
||||
:
|
||||
<div className="flex flex-col justify-end">
|
||||
<div className="h-[10px] my-[5px] w-20 animate-pulse rounded bg-gray-500" />
|
||||
<div className="h-[10px] my-[5px] w-10 animate-pulse rounded bg-gray-500 ml-auto" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
refuelAmount &&
|
||||
<div
|
||||
className="flex items-center justify-between w-full ">
|
||||
<div className='flex items-center gap-3 text-sm'>
|
||||
<span className="relative z-10 flex h-8 w-8 items-center justify-center rounded-full p-2 bg-primary/20">
|
||||
<Fuel className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||
</span>
|
||||
<p>Refuel</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-primary-text text-sm">{truncatedRefuelAmount} {nativeCurrency?.asset}</p>
|
||||
<p className="text-secondary-text text-sm flex justify-end">${refuelAmountInUsd}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Summary
|
||||
@@ -0,0 +1,103 @@
|
||||
import { FC } from "react"
|
||||
import useSWR from 'swr'
|
||||
import { useSettingsState } from "../../../context/settings"
|
||||
import { useSwapDataState } from "../../../context/swap"
|
||||
import Summary from "./Summary"
|
||||
import { ApiResponse } from "../../../Models/ApiResponse"
|
||||
import BridgeApiClient, { DepositType, Fee, TransactionType, WithdrawType } from "../../../lib/BridgeApiClient"
|
||||
import { GetDefaultNetwork } from "../../../helpers/settingsHelper"
|
||||
import useWalletTransferOptions from "../../../hooks/useWalletTransferOptions"
|
||||
|
||||
const SwapSummary: FC = () => {
|
||||
const { layers, currencies, networks } = useSettingsState()
|
||||
const { swap, withdrawType, selectedAssetNetwork } = useSwapDataState()
|
||||
const {
|
||||
source_network: source_network_internal_name,
|
||||
source_exchange: source_exchange_internal_name,
|
||||
destination_exchange: destination_exchange_internal_name,
|
||||
destination_network: destination_network_internal_name,
|
||||
source_network_asset,
|
||||
destination_network_asset,
|
||||
} = swap || {}
|
||||
|
||||
const { canDoSweepless, isContractWallet } = useWalletTransferOptions()
|
||||
|
||||
const params = {
|
||||
source: selectedAssetNetwork?.network?.internal_name,
|
||||
destination: destination_exchange_internal_name ?? destination_network_internal_name,
|
||||
source_asset: source_network_asset,
|
||||
destination_asset: destination_network_asset,
|
||||
refuel: swap?.has_refuel
|
||||
}
|
||||
|
||||
const apiClient = new BridgeApiClient()
|
||||
const { data: feeData } = useSWR<ApiResponse<Fee[]>>([params], selectedAssetNetwork ? ([params]) => apiClient.GetFee(params) : null, { dedupingInterval: 60000 })
|
||||
|
||||
const source_layer = layers.find(n => n.internal_name === (source_exchange_internal_name ?? source_network_internal_name))
|
||||
const asset = source_layer?.assets?.find(currency => currency?.asset === destination_network_asset)
|
||||
const currency = currencies?.find(c => c.asset === asset?.asset)
|
||||
const destination_layer = layers?.find(l => l.internal_name === (destination_exchange_internal_name ?? destination_network_internal_name))
|
||||
|
||||
if (!swap || !source_layer || !currency || !destination_layer) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const swapInputTransaction = swap?.transactions?.find(t => t.type === TransactionType.Input)
|
||||
const swapOutputTransaction = swap?.transactions?.find(t => t.type === TransactionType.Output)
|
||||
const swapRefuelTransaction = swap?.transactions?.find(t => t.type === TransactionType.Refuel)
|
||||
|
||||
let fee: number | undefined
|
||||
let min_amount: number | undefined
|
||||
|
||||
const walletTransferFee = feeData?.data?.find(f => f?.deposit_type === DepositType.Wallet)
|
||||
const manualTransferFee = feeData?.data?.find(f => f?.deposit_type === DepositType.Manual)
|
||||
|
||||
if (isContractWallet?.ready) {
|
||||
if (withdrawType === WithdrawType.Wallet && canDoSweepless) {
|
||||
fee = walletTransferFee?.fee_amount;
|
||||
min_amount = walletTransferFee?.min_amount;
|
||||
} else {
|
||||
fee = manualTransferFee?.fee_amount;
|
||||
min_amount = manualTransferFee?.min_amount;
|
||||
}
|
||||
}
|
||||
|
||||
if (swap?.fee && swapOutputTransaction) {
|
||||
fee = swap?.fee
|
||||
}
|
||||
|
||||
const requested_amount = (swapInputTransaction?.amount ??
|
||||
(Number(min_amount) > Number(swap.requested_amount) ? min_amount : swap.requested_amount)) || undefined
|
||||
|
||||
const destinationNetworkNativeAsset = currencies?.find(c => c.asset == networks.find(n => n.internal_name === destination_layer?.internal_name)?.native_currency);
|
||||
const destinationNetwork = GetDefaultNetwork(destination_layer, currency?.asset)
|
||||
const refuel_amount_in_usd = Number(destinationNetwork?.refuel_amount_in_usd)
|
||||
const native_usd_price = Number(destinationNetworkNativeAsset?.usd_price)
|
||||
const currency_usd_price = Number(currency?.usd_price)
|
||||
|
||||
const refuelAmountInNativeCurrency = swap?.has_refuel
|
||||
? ((swapRefuelTransaction?.amount ??
|
||||
(refuel_amount_in_usd / native_usd_price))) : undefined;
|
||||
|
||||
const refuelAmountInSelectedCurrency = swap?.has_refuel &&
|
||||
(refuel_amount_in_usd / currency_usd_price);
|
||||
|
||||
const receive_amount = fee != undefined ? (swapOutputTransaction?.amount
|
||||
?? (Number(requested_amount) - fee - Number(refuelAmountInSelectedCurrency)))
|
||||
: undefined
|
||||
|
||||
return <Summary
|
||||
currency={currency}
|
||||
source={source_layer}
|
||||
destination={destination_layer}
|
||||
requestedAmount={requested_amount as number}
|
||||
receiveAmount={receive_amount}
|
||||
destinationAddress={swap.destination_address}
|
||||
hasRefuel={swap?.has_refuel}
|
||||
refuelAmount={refuelAmountInNativeCurrency}
|
||||
fee={fee}
|
||||
exchange_account_connected={swap?.exchange_account_connected}
|
||||
exchange_account_name={swap?.exchange_account_name}
|
||||
/>
|
||||
}
|
||||
export default SwapSummary
|
||||
@@ -0,0 +1,195 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSettingsState } from '../../../../context/settings';
|
||||
import { useSwapDataState } from '../../../../context/swap';
|
||||
import { useInterval } from '../../../../hooks/useInterval';
|
||||
import { CalculateMinimalAuthorizeAmount } from '../../../../lib/fees';
|
||||
import { parseJwt } from '../../../../lib/jwtParser';
|
||||
import BridgeApiClient, { WithdrawType } from '../../../../lib/BridgeApiClient';
|
||||
import { OpenLink } from '../../../../lib/openLink';
|
||||
import TokenService from '../../../../lib/TokenService';
|
||||
import SubmitButton from '../../../buttons/submitButton';
|
||||
import Carousel, { CarouselItem, CarouselRef } from '../../../Carousel';
|
||||
import { FirstScreen, FourthScreen, LastScreen, SecondScreen, ThirdScreen } from './ConnectGuideScreens';
|
||||
import KnownInternalNames from '../../../../lib/knownIds';
|
||||
import { Layer } from '../../../../Models/Layer';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import IconButton from '../../../buttons/iconButton';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useCoinbaseStore } from './CoinbaseStore';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Widget } from '../../../Widget/Index';
|
||||
|
||||
type Props = {
|
||||
onAuthorized: () => void,
|
||||
onDoNotConnect: () => void,
|
||||
stickyFooter: boolean,
|
||||
hideHeader?: boolean,
|
||||
}
|
||||
|
||||
const Authorize: FC<Props> = ({ onAuthorized, stickyFooter, onDoNotConnect, hideHeader }) => {
|
||||
const { swap } = useSwapDataState()
|
||||
const { layers, currencies, discovery } = useSettingsState()
|
||||
const router = useRouter()
|
||||
let alreadyFamiliar = useCoinbaseStore((state) => state.alreadyFamiliar);
|
||||
let toggleAlreadyFamiliar = useCoinbaseStore((state) => state.toggleAlreadyFamiliar);
|
||||
const [carouselFinished, setCarouselFinished] = useState(alreadyFamiliar)
|
||||
|
||||
const [authWindow, setAuthWindow] = useState<Window | null>()
|
||||
const [firstScreen, setFirstScreen] = useState<boolean>(true)
|
||||
|
||||
const carouselRef = useRef<CarouselRef | null>(null)
|
||||
const exchange_internal_name = swap?.source_exchange
|
||||
const asset_name = swap?.source_network_asset
|
||||
|
||||
const exchange = layers.find(e => e.isExchange && e.internal_name?.toLowerCase() === exchange_internal_name?.toLowerCase()) as Layer & { isExchange: true }
|
||||
const currency = currencies?.find(c => asset_name?.toLocaleUpperCase() === c.asset?.toLocaleUpperCase())
|
||||
|
||||
const oauthProviders = discovery?.o_auth_providers
|
||||
const coinbaseOauthProvider = oauthProviders?.find(p => p.provider === KnownInternalNames.Exchanges.Coinbase)
|
||||
const { oauth_authorize_url } = coinbaseOauthProvider || {}
|
||||
|
||||
const minimalAuthorizeAmount = currency?.usd_price ?
|
||||
CalculateMinimalAuthorizeAmount(currency?.usd_price, Number(swap?.requested_amount)) : null
|
||||
|
||||
const checkShouldStartPolling = useCallback(() => {
|
||||
let authWindowHref: string | undefined = ""
|
||||
try {
|
||||
authWindowHref = authWindow?.location?.href
|
||||
}
|
||||
catch (e) {
|
||||
//throws error when accessing href TODO research safe way
|
||||
}
|
||||
if (authWindowHref && authWindowHref?.indexOf(window.location.origin) !== -1) {
|
||||
authWindow?.close()
|
||||
onAuthorized()
|
||||
}
|
||||
}, [authWindow])
|
||||
|
||||
useInterval(
|
||||
checkShouldStartPolling,
|
||||
authWindow && !authWindow.closed ? 1000 : null,
|
||||
)
|
||||
|
||||
const handleConnect = useCallback(() => {
|
||||
try {
|
||||
if (!swap)
|
||||
return
|
||||
if (!carouselFinished && !alreadyFamiliar) {
|
||||
carouselRef?.current?.next()
|
||||
return;
|
||||
}
|
||||
const access_token = TokenService.getAuthData()?.access_token
|
||||
if (!access_token) {
|
||||
//TODO handle not authenticated
|
||||
return
|
||||
}
|
||||
const { sub } = parseJwt(access_token) || {}
|
||||
const encoded = btoa(JSON.stringify({ SwapId: swap?.id, UserId: sub, RedirectUrl: `${window.location.origin}/salon` }))
|
||||
const authWindow = OpenLink({ link: oauth_authorize_url + encoded, query: router.query, swapId: swap.id })
|
||||
setAuthWindow(authWindow)
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}, [carouselFinished, alreadyFamiliar, swap?.id, oauth_authorize_url, router.query])
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
carouselRef?.current?.prev()
|
||||
return;
|
||||
}, [])
|
||||
|
||||
const exchange_name = exchange?.display_name
|
||||
|
||||
const onCarouselLast = (value) => {
|
||||
setCarouselFinished(value)
|
||||
}
|
||||
|
||||
const handleToggleChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
carouselRef?.current?.goToLast();
|
||||
} else {
|
||||
carouselRef?.current?.goToFirst();
|
||||
}
|
||||
toggleAlreadyFamiliar();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget.Content>
|
||||
{
|
||||
!hideHeader ?
|
||||
<h3 className='md:mb-4 pt-2 text-lg sm:text-xl text-left font-roboto text-primary-text font-semibold'>
|
||||
Please connect your {exchange_name} account
|
||||
</h3>
|
||||
: <></>
|
||||
}
|
||||
{
|
||||
<div className="w-full flex flex-col self-center h-[100%]">
|
||||
{swap && <Carousel onLast={onCarouselLast} onFirst={setFirstScreen} ref={carouselRef} starAtLast={alreadyFamiliar}>
|
||||
<CarouselItem width={100} >
|
||||
<FirstScreen exchange_name={exchange_name} />
|
||||
</CarouselItem>
|
||||
<CarouselItem width={100}>
|
||||
<SecondScreen />
|
||||
</CarouselItem>
|
||||
<CarouselItem width={100}>
|
||||
<ThirdScreen minimalAuthorizeAmount={minimalAuthorizeAmount} />
|
||||
</CarouselItem>
|
||||
<CarouselItem width={100}>
|
||||
<FourthScreen minimalAuthorizeAmount={minimalAuthorizeAmount} />
|
||||
</CarouselItem>
|
||||
<CarouselItem width={100}>
|
||||
<LastScreen number={!alreadyFamiliar} minimalAuthorizeAmount={Number(minimalAuthorizeAmount)} />
|
||||
</CarouselItem>
|
||||
</Carousel>}
|
||||
</div>
|
||||
}
|
||||
</Widget.Content>
|
||||
<Widget.Footer sticky={true}>
|
||||
<div>
|
||||
{
|
||||
<div className="flex items-center mb-3">
|
||||
<input
|
||||
name="alreadyFamiliar"
|
||||
id='alreadyFamiliar'
|
||||
type="checkbox"
|
||||
className="h-4 w-4 bg-secondary-600 cursor-pointer rounded border-secondary-400 text-priamry"
|
||||
onChange={handleToggleChange}
|
||||
checked={alreadyFamiliar}
|
||||
/>
|
||||
<label htmlFor="alreadyFamiliar" className="ml-2 cursor-pointer block text-sm text-primary-text">
|
||||
I'm already familiar with the process.
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
<div className='flex items-center'>
|
||||
{(!firstScreen && !alreadyFamiliar) &&
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<IconButton onClick={handlePrev} className='mr-4 py-3 px-3' icon={<ArrowLeft strokeWidth="3" />} />
|
||||
</motion.div>
|
||||
}
|
||||
<SubmitButton isDisabled={false} isSubmitting={false} onClick={handleConnect}>
|
||||
{
|
||||
carouselFinished ? "Connect" : "Next"
|
||||
}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
}
|
||||
<div className="pt-2 font-normal text-xs text-secondary-text">
|
||||
<p className="block font-lighter text-left">
|
||||
<span>Even after authorization Bridge can't initiate a withdrawal without your explicit confirmation. </span>
|
||||
<a target='_blank' href='https://docs.bridge.lux.network/user-docs/connect-a-coinbase-account' className='text-primary-text underline hover:no-underline decoration-white cursor-pointer'>Learn more</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</Widget.Footer>
|
||||
</ >
|
||||
)
|
||||
}
|
||||
|
||||
export default Authorize;
|
||||
@@ -0,0 +1,218 @@
|
||||
import { Info, ScanFace } from 'lucide-react';
|
||||
import { Form, Formik, FormikErrors, FormikProps } from 'formik';
|
||||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSwapDataState } from '../../../../context/swap';
|
||||
import { useTimerState } from '../../../../context/timerContext';
|
||||
import BridgeApiClient from '../../../../lib/BridgeApiClient';
|
||||
import { ApiError, LSAPIKnownErrorCode } from '../../../../Models/ApiError';
|
||||
import SubmitButton from '../../../buttons/submitButton';
|
||||
import SpinIcon from '../../../icons/spinIcon';
|
||||
import NumericInput from '../../../Input/NumericInput';
|
||||
import MessageComponent from '../../../MessageComponent';
|
||||
import Modal from '../../../modal/modal';
|
||||
import TimerWithContext from '../../../TimerComponent';
|
||||
import { Widget } from '../../../Widget/Index';
|
||||
|
||||
const TIMER_SECONDS = 120
|
||||
|
||||
interface CodeFormValues {
|
||||
Code: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onSuccess: (swapId: string) => Promise<void>
|
||||
footerStickiness?: boolean
|
||||
}
|
||||
|
||||
//TODO email code is almost identical create reusable component for email and two factor code verification
|
||||
const Coinbase2FA: FC<Props> = ({ onSuccess, footerStickiness = true }) => {
|
||||
const initialValues: CodeFormValues = { Code: '' }
|
||||
const { swap } = useSwapDataState()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showInsufficientFundsModal, setShowInsufficientFundsModal] = useState(false)
|
||||
const [showFundsOnHoldModal, setShowFundsOnHoldModal] = useState(false)
|
||||
|
||||
const { start: startTimer } = useTimerState()
|
||||
|
||||
const formikRef = useRef<FormikProps<CodeFormValues>>(null);
|
||||
|
||||
const handleSubmit = useCallback(async (values: CodeFormValues) => {
|
||||
if (!swap || !swap.source_exchange)
|
||||
return
|
||||
setLoading(true)
|
||||
try {
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
await bridgeApiClient.WithdrawFromExchange(swap.id, swap.source_exchange, values.Code)
|
||||
await onSuccess(swap.id)
|
||||
}
|
||||
catch (error) {
|
||||
const data: ApiError = error?.response?.data?.error
|
||||
|
||||
if (!data) {
|
||||
toast.error(error.message)
|
||||
return
|
||||
}
|
||||
else if (data.code === LSAPIKnownErrorCode.INSUFFICIENT_FUNDS) {
|
||||
setShowInsufficientFundsModal(true)
|
||||
}
|
||||
else if (data.code === LSAPIKnownErrorCode.FUNDS_ON_HOLD) {
|
||||
setShowFundsOnHoldModal(true)
|
||||
}
|
||||
else {
|
||||
toast.error(data.message)
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
}, [swap])
|
||||
|
||||
const handleResendTwoFACode = useCallback(async () => {
|
||||
if (!swap || !swap.source_exchange)
|
||||
return
|
||||
setLoading(true)
|
||||
try {
|
||||
formikRef.current?.setFieldValue("Code", "");
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
await bridgeApiClient.WithdrawFromExchange(swap.id, swap.source_exchange)
|
||||
} catch (error) {
|
||||
const data: ApiError = error?.response?.data?.error
|
||||
|
||||
if (!data) {
|
||||
toast.error(error.message)
|
||||
return
|
||||
}
|
||||
if (data.code === LSAPIKnownErrorCode.COINBASE_INVALID_2FA) {
|
||||
startTimer(TIMER_SECONDS)
|
||||
return
|
||||
}
|
||||
else {
|
||||
toast.error(data.message)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [swap])
|
||||
return <>
|
||||
<Modal show={showInsufficientFundsModal} setShow={setShowInsufficientFundsModal} >
|
||||
<MessageComponent>
|
||||
<MessageComponent.Content center icon='red'>
|
||||
<MessageComponent.Header>
|
||||
Transfer failed
|
||||
</MessageComponent.Header>
|
||||
<MessageComponent.Description>
|
||||
This transfer can't be processed because you don't have enough available funds on Coinbase.
|
||||
</MessageComponent.Description>
|
||||
</MessageComponent.Content>
|
||||
<MessageComponent.Buttons>
|
||||
<SubmitButton isDisabled={false} isSubmitting={false} onClick={() => {
|
||||
window.open("https://www.coinbase.com/", "_blank")
|
||||
}}>
|
||||
Check Coinbase
|
||||
</SubmitButton>
|
||||
</MessageComponent.Buttons>
|
||||
</MessageComponent>
|
||||
</Modal>
|
||||
<Modal show={showFundsOnHoldModal} setShow={setShowFundsOnHoldModal} >
|
||||
<MessageComponent>
|
||||
<MessageComponent.Content center icon='red'>
|
||||
<MessageComponent.Header>
|
||||
Transfer failed
|
||||
</MessageComponent.Header>
|
||||
<MessageComponent.Description>
|
||||
This transfer can't be processed because your funds might be on hold on Coinbase. This usually happens when you want to cash out immediately after completeing a purchare or adding cash.
|
||||
</MessageComponent.Description>
|
||||
</MessageComponent.Content>
|
||||
<MessageComponent.Buttons>
|
||||
<SubmitButton isDisabled={false} isSubmitting={false} onClick={() => {
|
||||
window.open("https://help.coinbase.com/en/coinbase/trading-and-funding/sending-or-receiving-cryptocurrency/available-balance-faq", "_blank")
|
||||
}}>
|
||||
Learn More
|
||||
</SubmitButton>
|
||||
</MessageComponent.Buttons>
|
||||
</MessageComponent>
|
||||
</Modal>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validateOnMount={true}
|
||||
innerRef={formikRef}
|
||||
validate={(values: CodeFormValues) => {
|
||||
const errors: FormikErrors<CodeFormValues> = {};
|
||||
if (!/^[0-9]*$/.test(values.Code)) {
|
||||
errors.Code = "Value should be numeric";
|
||||
}
|
||||
else if (values.Code.length != 7 && values.Code.length != 6) {
|
||||
errors.Code = `The length should be 6 or 7 instead of ${values.Code.length}`;
|
||||
}
|
||||
return errors;
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, isSubmitting, errors, handleChange }) => (
|
||||
<Form className='flex flex-col jutsify-center text-secondary-text h-full '>
|
||||
<Widget.Content center>
|
||||
<div className="w-full flex-col justify-between flex h-full">
|
||||
<ScanFace className='w-12 h-12 md:w-16 md:h-16 mt-auto text-primary self-center' />
|
||||
<div className='text-center md:mt-5 md:mb-8'>
|
||||
<p className='mb-2 md:mb-6 mt-2 pt-2 text-2xl font-bold text-primary-text leading-6 text-center font-roboto'>
|
||||
Coinbase 2FA
|
||||
</p>
|
||||
<p className='text-center text-base px-2'>
|
||||
Please enter the 2 step verification code of your Coinbase account.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative rounded-md shadow-sm mt-2 md:mt-5">
|
||||
<NumericInput
|
||||
pattern='^[0-9]*$'
|
||||
placeholder="XXXXXXX"
|
||||
maxLength={7}
|
||||
name='Code'
|
||||
onChange={e => {
|
||||
/^[0-9]*$/.test(e.target.value) && handleChange(e)
|
||||
}}
|
||||
className="leading-none h-12 text-2xl pl-5 text-primary-text focus:ring-primary text-center focus:border-primary border-secondary-500 block
|
||||
placeholder:text-2xl placeholder:text-center tracking-widest placeholder:font-normal placeholder:opacity-50 bg-secondary-700 w-full font-semibold rounded-md placeholder-primary-text"
|
||||
/>
|
||||
</div>
|
||||
<span className="flex text-sm leading-6 items-center mt-1.5">
|
||||
<TimerWithContext seconds={120}
|
||||
waitingComponent={(remainingTime) => (
|
||||
<span>
|
||||
<span>Resend in</span>
|
||||
<span className='ml-1'>
|
||||
{remainingTime}
|
||||
</span>
|
||||
</span>
|
||||
)}>
|
||||
{!loading ? <span onClick={handleResendTwoFACode} className="decoration underline-offset-1 underline hover:no-underline decoration-primary hover:cursor-pointer">
|
||||
Resend code
|
||||
</span>
|
||||
: <SpinIcon className="animate-spin h-5 w-5" />}
|
||||
</TimerWithContext>
|
||||
</span>
|
||||
<div className='p-4 bg-secondary-700 text-secondary-text rounded-lg border border-secondary-500 my-4'>
|
||||
<div className="flex items-center">
|
||||
<Info className='h-5 w-5 text-primary-600 mr-3' />
|
||||
<label className="block text-sm md:text-base font-medium leading-6">To obtain the 2 step verification code, check:</label>
|
||||
</div>
|
||||
<ul className="list-disc font-light space-y-1 text-xs md:text-sm mt-2 ml-8 text-left">
|
||||
<li>your authenticator app (Google, Microsoft, or other), or</li>
|
||||
<li>text messages of the phone number associated with your Coinbase account</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Widget.Content>
|
||||
<Widget.Footer sticky={footerStickiness}>
|
||||
<div className='md:mb-5'>
|
||||
<SubmitButton type="submit" isDisabled={!isValid || loading} isSubmitting={isSubmitting || loading}>
|
||||
Confirm
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Widget.Footer>
|
||||
</Form >
|
||||
)}
|
||||
</Formik>
|
||||
</>;
|
||||
}
|
||||
|
||||
export default Coinbase2FA;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
interface CoinbaseState {
|
||||
alreadyFamiliar: boolean
|
||||
toggleAlreadyFamiliar: () => void
|
||||
}
|
||||
|
||||
export const useCoinbaseStore = create<CoinbaseState>()(persist((set) => ({
|
||||
alreadyFamiliar: false,
|
||||
toggleAlreadyFamiliar: () => set((state) => ({ alreadyFamiliar: !state.alreadyFamiliar })),
|
||||
}),
|
||||
{
|
||||
name: 'coinbase-config-storage'
|
||||
}))
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,148 @@
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import SubmitButton from '../../../buttons/submitButton';
|
||||
import Modal from '../../../modal/modal';
|
||||
import Authorize from './Authorize';
|
||||
import Coinbase2FA from './Coinbase2FA';
|
||||
import { ArrowLeftRight, Link } from 'lucide-react';
|
||||
import { useSwapDataState, useSwapDataUpdate } from '../../../../context/swap';
|
||||
import BridgeApiClient, { PublishedSwapTransactionStatus } from '../../../../lib/BridgeApiClient';
|
||||
import { LSAPIKnownErrorCode } from '../../../../Models/ApiError';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSettingsState } from '../../../../context/settings';
|
||||
import { TimerProvider, useTimerState } from '../../../../context/timerContext';
|
||||
import { useSwapTransactionStore } from '../../../../stores/swapTransactionStore';
|
||||
const TIMER_SECONDS = 120
|
||||
|
||||
const Coinbase: FC = () => {
|
||||
return <TimerProvider>
|
||||
<TransferElements />
|
||||
</TimerProvider>
|
||||
}
|
||||
|
||||
const TransferElements: FC = () => {
|
||||
const { swap, codeRequested } = useSwapDataState()
|
||||
const { setCodeRequested, mutateSwap } = useSwapDataUpdate()
|
||||
const { networks } = useSettingsState()
|
||||
const {
|
||||
destination_network: destination_network_internal_name,
|
||||
} = swap || {}
|
||||
const { start: startTimer } = useTimerState()
|
||||
const { setSwapTransaction } = useSwapTransactionStore();
|
||||
|
||||
const [showCoinbaseConnectModal, setShowCoinbaseConnectModal] = useState(false)
|
||||
const [openCoinbase2FA, setOpenCoinbase2FA] = useState(false)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const destination_network = networks.find(n => n.internal_name === destination_network_internal_name)
|
||||
|
||||
const handleTransfer = useCallback(async () => {
|
||||
if (!swap || !swap.source_exchange)
|
||||
return
|
||||
setLoading(true)
|
||||
if (codeRequested)
|
||||
setOpenCoinbase2FA(true)
|
||||
else {
|
||||
try {
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
await bridgeApiClient.WithdrawFromExchange(swap.id, swap.source_exchange)
|
||||
}
|
||||
catch (e) {
|
||||
if (e?.response?.data?.error?.code === LSAPIKnownErrorCode.COINBASE_INVALID_2FA) {
|
||||
startTimer(TIMER_SECONDS)
|
||||
setCodeRequested(true)
|
||||
setOpenCoinbase2FA(true)
|
||||
}
|
||||
else if (e?.response?.data?.error?.code === LSAPIKnownErrorCode.INVALID_CREDENTIALS || e?.response?.data?.error?.code === LSAPIKnownErrorCode.COINBASE_AUTHORIZATION_LIMIT_EXCEEDED) {
|
||||
setCodeRequested(false)
|
||||
alert("You have not authorized enough to be able to complete the transfer. Please authorize again.")
|
||||
}
|
||||
else if (e?.response?.data?.error?.message) {
|
||||
toast(e?.response?.data?.error?.message)
|
||||
}
|
||||
else if (e?.message)
|
||||
toast(e.message)
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
}, [swap, destination_network, codeRequested])
|
||||
|
||||
const openConnect = () => {
|
||||
setShowCoinbaseConnectModal(true)
|
||||
}
|
||||
|
||||
const handleSuccess = useCallback(async (swapId: string) => {
|
||||
setOpenCoinbase2FA(false)
|
||||
setSwapTransaction(swapId, PublishedSwapTransactionStatus.Completed, "_")
|
||||
}, [])
|
||||
|
||||
const handleAuthorized = async () => {
|
||||
setLoading(true);
|
||||
setShowCoinbaseConnectModal(false)
|
||||
await mutateSwap()
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
height='90%'
|
||||
show={showCoinbaseConnectModal}
|
||||
setShow={setShowCoinbaseConnectModal}
|
||||
header={`Connect your Coinbase account`}
|
||||
>
|
||||
<Authorize
|
||||
hideHeader
|
||||
onDoNotConnect={() => setShowCoinbaseConnectModal(false)}
|
||||
onAuthorized={handleAuthorized}
|
||||
stickyFooter={false}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
height='90%'
|
||||
show={openCoinbase2FA}
|
||||
setShow={setOpenCoinbase2FA}
|
||||
>
|
||||
<Coinbase2FA
|
||||
onSuccess={handleSuccess}
|
||||
footerStickiness={false}
|
||||
/>
|
||||
</Modal>
|
||||
<div className="w-full space-y-5 flex flex-col justify-between h-full text-secondary-text">
|
||||
<div className='space-y-4'>
|
||||
<div className='border-secondary-500 rounded-md border bg-secondary-700 p-3'>
|
||||
{
|
||||
swap?.exchange_account_connected ?
|
||||
<SubmitButton
|
||||
isDisabled={loading}
|
||||
isSubmitting={loading}
|
||||
onClick={handleTransfer}
|
||||
icon={<ArrowLeftRight
|
||||
className="h-5 w-5 ml-2"
|
||||
aria-hidden="true"
|
||||
/>}
|
||||
>
|
||||
Transfer using Coinbase
|
||||
</SubmitButton>
|
||||
:
|
||||
<SubmitButton
|
||||
isDisabled={loading}
|
||||
isSubmitting={loading}
|
||||
onClick={openConnect}
|
||||
icon={<Link
|
||||
className="h-5 w-5 ml-2"
|
||||
aria-hidden="true" />
|
||||
}
|
||||
>
|
||||
Connect Coinbase account
|
||||
</SubmitButton>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Coinbase;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { FC } from 'react'
|
||||
import { Widget } from '../../Widget/Index';
|
||||
|
||||
|
||||
const External: FC = () => {
|
||||
|
||||
|
||||
return (
|
||||
<Widget.Content>
|
||||
<div className="w-full py-12 grid grid-flow-row">
|
||||
<div className='md:text-3xl text-lg font-bold text-primary-text leading-6 text-center'>
|
||||
Withdrawal pending
|
||||
</div>
|
||||
<div className='flex place-content-center mt-20 mb-16 md:mb-8'>
|
||||
<div className='relative'>
|
||||
<div className='absolute top-1 left-1 w-10 h-10 opacity-40 bg bg-primary rounded-full animate-ping'></div>
|
||||
<div className='absolute top-2 left-2 w-8 h-8 opacity-40 bg bg-primary rounded-full animate-ping'></div>
|
||||
<div className='relative top-0 left-0 w-12 h-12 scale-75 bg bg-primary-800 rounded-full'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col text-center place-content-center mt-1 text-lg font-lighter text-secondary-text">
|
||||
<p className="text-base font-medium space-y-6 text-secondary-text text-center">
|
||||
The withdrawal has been initiated, please don't close this screen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Widget.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export default External;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { FC, useCallback, useEffect } from 'react'
|
||||
import { useSwapDataState } from '../../../context/swap';
|
||||
import { useIntercom } from 'react-use-intercom';
|
||||
import { useAuthState } from '../../../context/authContext';
|
||||
import { SwapStatus } from '../../../Models/SwapStatus';
|
||||
import { SwapItem } from '../../../lib/BridgeApiClient';
|
||||
import { TrackEvent } from '../../../pages/_document';
|
||||
import QuestionIcon from '../../icons/Question';
|
||||
import Link from 'next/link';
|
||||
|
||||
const Failed: FC = () => {
|
||||
const { swap } = useSwapDataState()
|
||||
const { email, userId } = useAuthState()
|
||||
const { boot, show, update } = useIntercom()
|
||||
const updateWithProps = () => update({ email: email, userId: userId, customAttributes: { swapId: swap?.id } })
|
||||
|
||||
useEffect(() => {
|
||||
window.plausible && plausible(TrackEvent.SwapFailed)
|
||||
}, [])
|
||||
|
||||
const startIntercom = useCallback(() => {
|
||||
boot();
|
||||
show();
|
||||
updateWithProps()
|
||||
}, [boot, show, updateWithProps])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative z-10 flex h-8 w-8 items-center justify-center rounded-full bg-primary/20">
|
||||
<QuestionIcon className="h-7 w-7 text-primary" aria-hidden="true" />
|
||||
</span>
|
||||
<label className="block text-sm md:text-base text-primary-text font-medium">What's happening?</label>
|
||||
</div>
|
||||
<div className='mt-4 text-xs md:text-sm text-primary-text'>
|
||||
{
|
||||
swap?.status == SwapStatus.Cancelled &&
|
||||
<Canceled onGetHelp={startIntercom} swap={swap} />
|
||||
}
|
||||
{
|
||||
swap?.status == SwapStatus.Expired &&
|
||||
<Expired onGetHelp={startIntercom} swap={swap} />
|
||||
}
|
||||
{
|
||||
swap?.status == SwapStatus.UserTransferDelayed &&
|
||||
<Delay />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
type Props = {
|
||||
swap: SwapItem
|
||||
onGetHelp: () => void
|
||||
}
|
||||
|
||||
const Expired = ({ onGetHelp }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<span className='text-md text-left text-xs md:text-sm text-primary-text'>The transfer wasn't completed during the allocated timeframe.</span>
|
||||
<span className='text-secondary-text'><span> If you've already sent crypto for this swap, your funds are safe, </span><a className='underline hover:cursor-pointer' onClick={() => onGetHelp()}>please contact our support.</a></span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Delay: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<p className='text-md '><span>This usually means that the exchange needs additional verification.</span>
|
||||
<Link target='_blank' href="https://docs.bridge.lux.network/user-docs/why-is-coinbase-transfer-taking-so-long"
|
||||
className='disabled:text-opacity-40 disabled:bg-primary-900 disabled:cursor-not-allowed ml-1 underline hover:no-underline cursor-pointer'>Learn More</Link></p>
|
||||
<ul className="list-inside list-decimal font-light space-y-1 mt-2 text-left text-primary-text ">
|
||||
<li>Check your email for details from Coinbase</li>
|
||||
<li>Check your Coinbase account's transfer history</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Canceled = ({ onGetHelp }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<p className='text-md text-left text-primary-text'><span>The transaction was cancelled by your request.</span>
|
||||
<span className='text-secondary-text'><span> If you've already sent crypto for this swap, your funds are safe,</span><a className='underline hover:cursor-pointer' onClick={() => onGetHelp()}> please contact our support.</a></span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Failed;
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Context, FC, createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useSwapDataState } from "../../../context/swap";
|
||||
import { StripeOnramp, loadStripeOnramp } from "@stripe/crypto";
|
||||
import { PublishedSwapTransactionStatus } from "../../../lib/BridgeApiClient";
|
||||
import { useSwapTransactionStore } from "../../../stores/swapTransactionStore";
|
||||
import inIframe from "../../utils/inIframe";
|
||||
import SubmitButton from "../../buttons/submitButton";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
type ContextState = {
|
||||
onramp: StripeOnramp | null
|
||||
}
|
||||
const FiatTransfer: FC = () => {
|
||||
const { swap } = useSwapDataState()
|
||||
const stripeSessionId = swap?.fiat_session_id
|
||||
const secret = process.env.NEXT_PUBLIC_STRIPE_SECRET || ""
|
||||
const stripeOnrampPromise = loadStripeOnramp(secret);
|
||||
|
||||
const [embedded, setEmbedded] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
setEmbedded(inIframe())
|
||||
}, [])
|
||||
|
||||
return <div className='rounded-xl bg-secondary-700 border border-secondary-500 divide-y divide-secondary-500'>
|
||||
{
|
||||
embedded ?
|
||||
<SubmitButton onClick={() => window.open(swap?.fiat_redirect_url, '_blank')} icon={<ExternalLink className='h-5 w-5' strokeWidth={2} />} isDisabled={!swap} isSubmitting={false}>
|
||||
Continue in Stripe
|
||||
</SubmitButton>
|
||||
:
|
||||
<CryptoElements stripeOnramp={stripeOnrampPromise}>
|
||||
{stripeSessionId && <OnrampElement clientSecret={stripeSessionId} swapId={swap.id} />}
|
||||
</CryptoElements>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
const CryptoElementsContext = createContext<ContextState | null>(null);
|
||||
|
||||
export const CryptoElements: FC<{ stripeOnramp: Promise<StripeOnramp | null>, children?: React.ReactNode }> = ({
|
||||
stripeOnramp,
|
||||
children
|
||||
}) => {
|
||||
const [ctx, setContext] = useState<{ onramp: StripeOnramp | null }>(() => ({ onramp: null }));
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
Promise.resolve(stripeOnramp).then((onramp) => {
|
||||
if (onramp && isMounted) {
|
||||
setContext((ctx) => (ctx.onramp ? ctx : { onramp }));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [stripeOnramp]);
|
||||
|
||||
return (
|
||||
<CryptoElementsContext.Provider value={ctx}>
|
||||
{children}
|
||||
</CryptoElementsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// React hook to get StripeOnramp from context
|
||||
export const useStripeOnramp = () => {
|
||||
const context = useContext<ContextState>(CryptoElementsContext as Context<ContextState>);
|
||||
return context?.onramp;
|
||||
};
|
||||
type OnrampElementProps = {
|
||||
clientSecret: string,
|
||||
swapId: string,
|
||||
}
|
||||
// React element to render Onramp UI
|
||||
export const OnrampElement: FC<OnrampElementProps> = ({
|
||||
clientSecret,
|
||||
swapId,
|
||||
}) => {
|
||||
const stripeOnramp = useStripeOnramp();
|
||||
const onrampElementRef = useRef<HTMLDivElement>(null);
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { setSwapTransaction } = useSwapTransactionStore();
|
||||
|
||||
useEffect(() => {
|
||||
const containerRef = onrampElementRef.current;
|
||||
if (containerRef) {
|
||||
containerRef.innerHTML = '';
|
||||
if (clientSecret && stripeOnramp && swapId) {
|
||||
setLoading(true)
|
||||
const session = stripeOnramp
|
||||
.createSession({
|
||||
clientSecret,
|
||||
appearance: {
|
||||
theme: "dark"
|
||||
},
|
||||
})
|
||||
.mount(containerRef)
|
||||
const eventListener = async (e) => {
|
||||
let transactionStatus: PublishedSwapTransactionStatus
|
||||
if (e.payload.session.status === "fulfillment_complete")
|
||||
transactionStatus = PublishedSwapTransactionStatus.Completed
|
||||
else if (e.payload.session.status === "fulfillment_processing")
|
||||
transactionStatus = PublishedSwapTransactionStatus.Pending
|
||||
else {
|
||||
// TODO handle
|
||||
return
|
||||
}
|
||||
await setSwapTransaction(swapId, PublishedSwapTransactionStatus.Completed, e.payload.session.id);
|
||||
}
|
||||
|
||||
session.addEventListener("onramp_session_updated", eventListener)
|
||||
session.addEventListener("onramp_ui_loaded", () => setLoading(false))
|
||||
}
|
||||
}
|
||||
|
||||
}, [clientSecret, stripeOnramp, swapId]);
|
||||
|
||||
return <div className="relative">
|
||||
{
|
||||
loading &&
|
||||
<Skeleton />
|
||||
}
|
||||
<div ref={onrampElementRef} ></div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
const Skeleton: FC = () => {
|
||||
return <div className="absolute pt-10 top-0 left-0 right-0 bottom-0 w-full overflow-hidden flex flex-col items-center justify-between">
|
||||
<div className="animate-pulse flex flex-col items-center justify-between w-full">
|
||||
<div className="rounded-md p-4 max-w-sm w-full mx-auto">
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="rounded-md bg-slate-700 h-6 w-6"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-2 w-12 bg-slate-700 rounded"></div>
|
||||
<div className="h-2 w-40 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border border-slate-700 grid grid-rows-2 gap-2 rounded-t-md p-4 max-w-sm w-full mx-auto">
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
<div className="h-2 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
<div className="h-6 bg-slate-700 rounded col-span-2"></div>
|
||||
<div className="h-4 col-start-6 bg-slate-700 rounded"></div>
|
||||
<div className="h-4 bg-slate-700 rounded"></div>
|
||||
<div className="h-4 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-rows-3 gap-2 rounded-md p-4 max-w-sm w-full mx-auto">
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
<div className="h-6 bg-slate-700 rounded col-span-2"></div>
|
||||
<div className="h-4 col-start-6 col-span-3 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-4">
|
||||
<div className="h-2 bg-slate-700 rounded"></div>
|
||||
<div className="h-2 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md p-4 max-w-sm w-full mx-auto">
|
||||
<div className="animate-pulse flex justify-between space-x-4">
|
||||
<div className="space-y-2">
|
||||
<div className="h-2 w-10 bg-slate-700 rounded"></div>
|
||||
<div className="h-2 w-6 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-2 w-8 bg-slate-700 rounded"></div>
|
||||
<div className="h-2 w-8 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="animate-pulse grid rounded-md p-4 max-w-sm w-full mx-auto">
|
||||
<div className="h-10 bg-slate-700 rounded col-span-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default FiatTransfer
|
||||
@@ -0,0 +1,275 @@
|
||||
import { FC, useCallback } from "react"
|
||||
import useSWR from "swr"
|
||||
import { ArrowLeftRight } from "lucide-react"
|
||||
import Image from 'next/image';
|
||||
import { ApiResponse } from "../../../Models/ApiResponse";
|
||||
import { useSettingsState } from "../../../context/settings";
|
||||
import { useSwapDataState, useSwapDataUpdate } from "../../../context/swap";
|
||||
import KnownInternalNames from "../../../lib/knownIds";
|
||||
import BackgroundField from "../../backgroundField";
|
||||
import BridgeApiClient, { DepositAddress, DepositAddressSource, DepositType, Fee } from "../../../lib/BridgeApiClient";
|
||||
import SubmitButton from "../../buttons/submitButton";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "../../shadcn/select";
|
||||
import { BaseL2Asset } from "../../../Models/Layer";
|
||||
import shortenAddress from "../../utils/ShortenAddress";
|
||||
import { isValidAddress } from "../../../lib/addressValidator";
|
||||
import { useSwapDepositHintClicked } from "../../../stores/swapTransactionStore";
|
||||
|
||||
const ManualTransfer: FC = () => {
|
||||
const { swap } = useSwapDataState()
|
||||
const hintsStore = useSwapDepositHintClicked()
|
||||
const hintClicked = hintsStore.swapTransactions[swap?.id || ""]
|
||||
const {
|
||||
source_network: source_network_internal_name } = swap || {}
|
||||
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
const {
|
||||
data: generatedDeposit,
|
||||
isLoading
|
||||
} = useSWR<ApiResponse<DepositAddress>>(`/deposit_addresses/${source_network_internal_name}?source=${DepositAddressSource.UserGenerated}`,
|
||||
bridgeApiClient.fetcher,
|
||||
{
|
||||
dedupingInterval: 60000,
|
||||
shouldRetryOnError: false
|
||||
}
|
||||
)
|
||||
|
||||
let generatedDepositAddress = generatedDeposit?.data?.address
|
||||
let shouldGenerateAddress = !generatedDepositAddress && hintClicked
|
||||
|
||||
const handleCloseNote = useCallback(async () => {
|
||||
if (swap)
|
||||
hintsStore.setSwapDepositHintClicked(swap?.id)
|
||||
}, [swap, hintsStore])
|
||||
|
||||
return (
|
||||
<div className='rounded-md bg-secondary-700 border border-secondary-500 w-full h-full items-center relative'>
|
||||
<div className={!hintClicked ? "absolute w-full h-full flex flex-col items-center px-3 pb-3 text-center" : "hidden"}>
|
||||
<div className="flex flex-col items-center justify-center h-full pb-2">
|
||||
<div className="max-w-xs">
|
||||
<p className="text-base text-primary-text">
|
||||
About manual transfers
|
||||
</p>
|
||||
<p className="text-xs text-secondary-text">
|
||||
Transfer assets to Bridge’s deposit address to complete the swap.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitButton isDisabled={false} isSubmitting={false} size="medium" onClick={handleCloseNote}>
|
||||
OK
|
||||
</SubmitButton>
|
||||
</div>
|
||||
<div className={hintClicked ? "flex" : "invisible"}>
|
||||
<TransferInvoice address={generatedDepositAddress} shouldGenerateAddress={shouldGenerateAddress} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
const TransferInvoice: FC<{ address?: string, shouldGenerateAddress: boolean }> = ({ address: existingDepositAddress, shouldGenerateAddress }) => {
|
||||
|
||||
const { layers, resolveImgSrc } = useSettingsState()
|
||||
const { swap, selectedAssetNetwork } = useSwapDataState()
|
||||
const { setSelectedAssetNetwork } = useSwapDataUpdate()
|
||||
const {
|
||||
source_network: source_network_internal_name,
|
||||
source_exchange: source_exchange_internal_name,
|
||||
destination_network: destination_network_internal_name,
|
||||
destination_network_asset,
|
||||
source_network_asset
|
||||
} = swap || {}
|
||||
|
||||
const source_exchange = layers.find(n => n.internal_name === source_exchange_internal_name)
|
||||
|
||||
const asset = selectedAssetNetwork?.network?.currencies.find(c => c.asset == destination_network_asset)
|
||||
|
||||
const bridgeApiClient = new BridgeApiClient()
|
||||
const generateDepositParams = shouldGenerateAddress ? [selectedAssetNetwork?.network_internal_name ?? null] : null
|
||||
|
||||
const {
|
||||
data: generatedDeposit
|
||||
} = useSWR<ApiResponse<DepositAddress>>(generateDepositParams, ([network]) => bridgeApiClient.GenerateDepositAddress(network), { dedupingInterval: 60000 })
|
||||
|
||||
const feeParams = {
|
||||
source: selectedAssetNetwork?.network?.internal_name,
|
||||
destination: destination_network_internal_name,
|
||||
source_asset: source_network_asset,
|
||||
destination_asset: destination_network_asset,
|
||||
refuel: swap?.has_refuel
|
||||
}
|
||||
|
||||
const { data: feeData } = useSWR<ApiResponse<Fee[]>>([feeParams], ([params]) => bridgeApiClient.GetFee(params), { dedupingInterval: 60000 })
|
||||
const manualTransferFee = feeData?.data?.find(f => f?.deposit_type === DepositType.Manual)
|
||||
|
||||
const requested_amount = Number(manualTransferFee?.min_amount) > Number(swap?.requested_amount) ? manualTransferFee?.min_amount : swap?.requested_amount
|
||||
const depositAddress = existingDepositAddress || generatedDeposit?.data?.address
|
||||
|
||||
const handleChangeSelectedNetwork = useCallback((n: BaseL2Asset) => {
|
||||
setSelectedAssetNetwork(n)
|
||||
}, [])
|
||||
|
||||
return <div className='divide-y divide-secondary-500 text-primary-text h-full'>
|
||||
{source_exchange && <div className={`w-full relative rounded-md px-3 py-3 shadow-sm border-secondary-700 border bg-secondary-700 flex flex-col items-center justify-center gap-2`}>
|
||||
<ExchangeNetworkPicker onChange={handleChangeSelectedNetwork} />
|
||||
</div>
|
||||
}
|
||||
<div className="flex divide-x divide-secondary-500">
|
||||
<BackgroundField Copiable={true} QRable={true} header={"Deposit address"} toCopy={depositAddress} withoutBorder>
|
||||
<div>
|
||||
{
|
||||
depositAddress ?
|
||||
<p className='break-all'>
|
||||
{depositAddress}
|
||||
</p>
|
||||
:
|
||||
<div className='bg-gray-500 w-56 h-5 animate-pulse rounded-md' />
|
||||
}
|
||||
{
|
||||
(source_network_internal_name === KnownInternalNames.Networks.LoopringMainnet || source_network_internal_name === KnownInternalNames.Networks.LoopringGoerli) &&
|
||||
<div className='flex text-xs items-center py-1 mt-1 border-2 border-secondary-300 rounded border-dashed text-secondary-text'>
|
||||
<p>
|
||||
This address might not be activated. You can ignore it.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
{
|
||||
(source_network_internal_name === KnownInternalNames.Networks.LoopringMainnet || source_network_internal_name === KnownInternalNames.Networks.LoopringGoerli) &&
|
||||
<div className='grid grid-cols-3 divide-x divide-secondary-500'>
|
||||
<div className="col-span-2">
|
||||
<BackgroundField header={'Send type'} withoutBorder>
|
||||
<div className='flex items-center text-xs sm:text-sm'>
|
||||
<ArrowLeftRight className='hidden sm:inline-block sm:h-4 sm:w-4' />
|
||||
<p>
|
||||
To Another Loopring L2 Account
|
||||
</p>
|
||||
</div>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
<BackgroundField header={'Address Type'} withoutBorder>
|
||||
<p className="text-xs sm:text-sm">
|
||||
EOA Wallet
|
||||
</p>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className='flex divide-x divide-secondary-500'>
|
||||
<BackgroundField Copiable={true} toCopy={requested_amount} header={'Amount'} withoutBorder>
|
||||
<p>
|
||||
{requested_amount}
|
||||
</p>
|
||||
</BackgroundField>
|
||||
<BackgroundField header={'Asset'} withoutBorder Explorable={asset?.contract_address != null && isValidAddress(asset?.contract_address, selectedAssetNetwork?.network)} toExplore={asset?.contract_address != null ? selectedAssetNetwork?.network?.account_explorer_template?.replace("{0}", asset?.contract_address) : undefined}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0 h-7 w-7 relative">
|
||||
{
|
||||
asset &&
|
||||
<Image
|
||||
src={resolveImgSrc({ asset: asset?.asset })}
|
||||
alt="From Logo"
|
||||
height="60"
|
||||
width="60"
|
||||
className="rounded-md object-contain"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold leading-4">
|
||||
{asset?.name}
|
||||
</span>
|
||||
{asset?.contract_address && isValidAddress(asset.contract_address, selectedAssetNetwork?.network) &&
|
||||
<span className="text-xs text-secondary-text flex items-center leading-3">
|
||||
{shortenAddress(asset?.contract_address)}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</BackgroundField>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const ExchangeNetworkPicker: FC<{ onChange: (network: BaseL2Asset) => void }> = ({ onChange }) => {
|
||||
const { layers, resolveImgSrc } = useSettingsState()
|
||||
const { swap } = useSwapDataState()
|
||||
const {
|
||||
source_exchange: source_exchange_internal_name,
|
||||
destination_network,
|
||||
source_network_asset } = swap || {}
|
||||
const source_exchange = layers.find(n => n.internal_name === source_exchange_internal_name)
|
||||
|
||||
const exchangeAssets = source_exchange?.assets?.filter(a => a.asset === source_network_asset && a.network_internal_name !== destination_network && a.network?.status !== "inactive")
|
||||
const defaultSourceNetwork = exchangeAssets?.find(sn => sn.is_default) || exchangeAssets?.[0]
|
||||
|
||||
const handleChangeSelectedNetwork = useCallback((n: string) => {
|
||||
const network = exchangeAssets?.find(network => network?.network_internal_name === n)
|
||||
if (network)
|
||||
onChange(network)
|
||||
}, [exchangeAssets])
|
||||
|
||||
return <div className='flex items-center gap-1 text-sm my-2'>
|
||||
<span>Network:</span>
|
||||
{exchangeAssets?.length === 1 ?
|
||||
<div className='flex space-x-1 items-center w-fit font-semibold text-primary-text'>
|
||||
<Image alt="chainLogo" height='20' width='20' className='h-5 w-5 rounded-md ring-2 ring-secondary-600' src={resolveImgSrc(exchangeAssets?.[0])}></Image>
|
||||
<span>{defaultSourceNetwork?.network?.display_name}</span>
|
||||
</div>
|
||||
:
|
||||
<Select onValueChange={handleChangeSelectedNetwork} defaultValue={defaultSourceNetwork?.network_internal_name}>
|
||||
<SelectTrigger className="w-fit border-none !text-primary-text !font-semibold !h-fit !p-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Networks</SelectLabel>
|
||||
{exchangeAssets?.map(sn => (
|
||||
<SelectItem key={sn.network_internal_name} value={sn.network_internal_name}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-5 w-5 relative">
|
||||
{
|
||||
sn.network &&
|
||||
<Image
|
||||
src={resolveImgSrc(sn.network)}
|
||||
alt="From Logo"
|
||||
height="60"
|
||||
width="60"
|
||||
className="rounded-md object-contain"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="mx-1 block">{sn?.network?.display_name}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const Sceleton = () => {
|
||||
return <div className="animate-pulse rounded-lg p-4 flex items-center text-center bg-secondary-700 border border-secondary-500">
|
||||
<div className="flex-1 space-y-6 py-1 p-8 pt-4 items-center">
|
||||
<div className="h-2 bg-secondary-text rounded self-center w-16 m-auto"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="h-2 bg-secondary-text rounded col-span-2"></div>
|
||||
<div className="h-2 bg-secondary-text rounded col-span-1"></div>
|
||||
</div>
|
||||
<div className="h-2 bg-secondary-text rounded"></div>
|
||||
|
||||
</div>
|
||||
<div className="h-2 bg-secondary-text rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default ManualTransfer
|
||||
@@ -0,0 +1,325 @@
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { FC } from 'react'
|
||||
import { Widget } from '../../../Widget/Index';
|
||||
import shortenAddress from '../../../utils/ShortenAddress';
|
||||
import Steps from '../../StepsComponent';
|
||||
import SwapSummary from '../../Summary';
|
||||
import { GetNetworkCurrency } from '../../../../helpers/settingsHelper';
|
||||
import AverageCompletionTime from '../../../Common/AverageCompletionTime';
|
||||
import { SwapItem, TransactionStatus, TransactionType } from '../../../../lib/BridgeApiClient';
|
||||
import { truncateDecimals } from '../../../utils/RoundDecimals';
|
||||
import { BridgeAppSettings } from '../../../../Models/BridgeAppSettings';
|
||||
import { SwapStatus } from '../../../../Models/SwapStatus';
|
||||
import { SwapFailReasons } from '../../../../Models/RangeError';
|
||||
import { Gauge } from '../../../gauge';
|
||||
import Failed from '../Failed';
|
||||
import { Progress, ProgressStates, ProgressStatus, StatusStep } from './types';
|
||||
import { useSwapTransactionStore } from '../../../../stores/swapTransactionStore';
|
||||
|
||||
type Props = {
|
||||
settings: BridgeAppSettings;
|
||||
swap: SwapItem;
|
||||
}
|
||||
|
||||
const Processing: FC<Props> = ({ settings, swap }) => {
|
||||
|
||||
const swapStatus = swap.status;
|
||||
const storedWalletTransactions = useSwapTransactionStore()
|
||||
|
||||
const source_network = settings.networks?.find(e => e.internal_name === swap.source_network)
|
||||
const destination_network = settings.networks?.find(e => e.internal_name === swap.destination_network)
|
||||
const destination_layer = settings.layers?.find(e => e.internal_name === swap.destination_network)
|
||||
|
||||
const input_tx_explorer = source_network?.transaction_explorer_template
|
||||
const output_tx_explorer = destination_network?.transaction_explorer_template
|
||||
|
||||
const destinationNetworkCurrency = destination_layer ? GetNetworkCurrency(destination_layer, swap?.destination_network_asset) : null
|
||||
|
||||
const swapInputTransaction = swap?.transactions?.find(t => t.type === TransactionType.Input)
|
||||
const storedWalletTransaction = storedWalletTransactions.swapTransactions?.[swap?.id]
|
||||
|
||||
const transactionHash = swapInputTransaction?.transaction_id || storedWalletTransaction?.hash
|
||||
|
||||
|
||||
const swapOutputTransaction = swap?.transactions?.find(t => t.type === TransactionType.Output)
|
||||
const swapRefuelTransaction = swap?.transactions?.find(t => t.type === TransactionType.Refuel)
|
||||
|
||||
const nativeCurrency = destination_layer?.isExchange === false ? settings?.currencies?.find(c => c.asset === destination_layer?.native_currency) : null
|
||||
const truncatedRefuelAmount = swapRefuelTransaction?.amount ? truncateDecimals(swapRefuelTransaction?.amount, nativeCurrency?.precision) : null
|
||||
|
||||
const progressStatuses = getProgressStatuses(swap, swapStatus)
|
||||
const stepStatuses = progressStatuses.stepStatuses;
|
||||
|
||||
const outputPendingDetails = <div className='flex items-center space-x-1'>
|
||||
<span>Estimated arrival after confirmation:</span>
|
||||
<div className='text-primary-text'>
|
||||
{
|
||||
destinationNetworkCurrency?.status == 'insufficient_liquidity' ?
|
||||
<span>Up to 2 hours (delayed)</span>
|
||||
:
|
||||
<AverageCompletionTime time={destination_network?.average_completion_time} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const progressStates: ProgressStates = {
|
||||
"input_transfer": {
|
||||
upcoming: {
|
||||
name: 'Waiting for your transfer',
|
||||
description: null
|
||||
},
|
||||
current: {
|
||||
name: 'Processing your deposit',
|
||||
description: <div>
|
||||
<span>
|
||||
<span>Waiting for confirmations</span>
|
||||
{swapInputTransaction && swapInputTransaction?.confirmations && (
|
||||
<span className="text-primary-text ml-1">
|
||||
<span>{swapInputTransaction?.confirmations >= swapInputTransaction?.max_confirmations
|
||||
? swapInputTransaction?.max_confirmations
|
||||
: swapInputTransaction?.confirmations}</span>
|
||||
<span>/</span>{swapInputTransaction?.max_confirmations}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
},
|
||||
complete: {
|
||||
name: `Your deposit is confirmed`,
|
||||
description: <div className='flex items-center space-x-1'>
|
||||
<span>Transaction: </span>
|
||||
<div className='underline hover:no-underline flex items-center space-x-1'>
|
||||
<a target={"_blank"} href={input_tx_explorer?.replace("{0}", transactionHash)}>{shortenAddress(transactionHash)}</a>
|
||||
<ExternalLink className='h-4' />
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
failed: {
|
||||
name: `The transfer failed`,
|
||||
description: <div className='flex space-x-1'>
|
||||
<span>Error: </span>
|
||||
<div className='space-x-1 text-primary-text'>
|
||||
{swap?.fail_reason == SwapFailReasons.RECEIVED_MORE_THAN_VALID_RANGE ?
|
||||
"Your deposit is higher than the max limit. We'll review and approve your transaction in up to 2 hours."
|
||||
:
|
||||
swap?.fail_reason == SwapFailReasons.RECEIVED_LESS_THAN_VALID_RANGE ?
|
||||
"Your deposit is lower than the minimum required amount. Unfortunately, we can't process the transaction. Please contact support to check if you're eligible for a refund."
|
||||
:
|
||||
"Something went wrong while processing the transfer. Please contact support"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
delayed: {
|
||||
name: `This transfer is being delayed by Coinbase`,
|
||||
description: null
|
||||
}
|
||||
},
|
||||
"output_transfer": {
|
||||
upcoming: {
|
||||
name: `Sending ${destinationNetworkCurrency?.name} to your address`,
|
||||
description: null
|
||||
},
|
||||
current: {
|
||||
name: `Sending ${destinationNetworkCurrency?.name} to your address`,
|
||||
description: null
|
||||
},
|
||||
complete: {
|
||||
name: `${swapOutputTransaction?.amount} ${swap?.destination_network_asset} was sent to your address`,
|
||||
description: swapOutputTransaction ? <div className="flex flex-col">
|
||||
<div className='flex items-center space-x-1'>
|
||||
<span>Transaction: </span>
|
||||
<div className='underline hover:no-underline flex items-center space-x-1'>
|
||||
<a target={"_blank"} href={output_tx_explorer?.replace("{0}", swapOutputTransaction.transaction_id)}>{shortenAddress(swapOutputTransaction.transaction_id)}</a>
|
||||
<ExternalLink className='h-4' />
|
||||
</div>
|
||||
</div>
|
||||
</div> : null,
|
||||
},
|
||||
failed: {
|
||||
name: swap?.fail_reason == SwapFailReasons.RECEIVED_MORE_THAN_VALID_RANGE ? `The transfer is on hold` : "The transfer has failed",
|
||||
description: <div className='flex space-x-1'>
|
||||
<div className='space-x-1 text-secondary-text'>
|
||||
{swap?.fail_reason == SwapFailReasons.RECEIVED_MORE_THAN_VALID_RANGE ?
|
||||
"Your deposit is higher than the max limit. We'll review and approve your transaction in up to 2 hours."
|
||||
:
|
||||
swap?.fail_reason == SwapFailReasons.RECEIVED_LESS_THAN_VALID_RANGE ?
|
||||
"Your deposit is lower than the minimum required amount. Unfortunately, we can't process the transaction. Please contact support to check if you're eligible for a refund."
|
||||
:
|
||||
"Something went wrong while processing the transfer. Please contact support"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
delayed: {
|
||||
name: `This swap is being delayed by Coinbase`,
|
||||
description: null
|
||||
}
|
||||
},
|
||||
"refuel": {
|
||||
upcoming: {
|
||||
name: `Sending ${nativeCurrency?.asset} to your address`,
|
||||
description: null
|
||||
},
|
||||
current: {
|
||||
name: `Sending ${nativeCurrency?.asset} to your address`,
|
||||
description: null
|
||||
},
|
||||
complete: {
|
||||
name: `${truncatedRefuelAmount} ${nativeCurrency?.asset} was sent to your address`,
|
||||
description: <div className='flex items-center space-x-1'>
|
||||
<span>Transaction: </span>
|
||||
<div className='underline hover:no-underline flex items-center space-x-1'>
|
||||
{swapRefuelTransaction && <>
|
||||
<a target={"_blank"} href={swapRefuelTransaction?.explorer_url}>{shortenAddress(swapRefuelTransaction?.transaction_id)}</a>
|
||||
<ExternalLink className='h-4' />
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
delayed: {
|
||||
name: `This transfers is being delayed`,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allSteps: StatusStep[] = [
|
||||
{
|
||||
name: progressStates.input_transfer?.[stepStatuses?.input_transfer]?.name,
|
||||
status: stepStatuses.input_transfer,
|
||||
description: progressStates?.input_transfer?.[stepStatuses?.input_transfer]?.description,
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
name: progressStates.output_transfer?.[stepStatuses?.output_transfer]?.name,
|
||||
status: stepStatuses.output_transfer,
|
||||
description: progressStates?.output_transfer?.[stepStatuses?.output_transfer]?.description,
|
||||
index: 2
|
||||
},
|
||||
{
|
||||
name: progressStates.refuel?.[stepStatuses?.refuel]?.name,
|
||||
status: stepStatuses.refuel,
|
||||
description: progressStates.refuel?.[stepStatuses?.refuel]?.description,
|
||||
index: 3
|
||||
}
|
||||
]
|
||||
|
||||
let currentSteps = allSteps.filter((s) => s.status && s.status != ProgressStatus.Removed);
|
||||
let stepsProgressPercentage = currentSteps.filter(x => x.status == ProgressStatus.Complete).length / currentSteps.length * 100;
|
||||
|
||||
if (!swap) return <></>
|
||||
return (
|
||||
<Widget.Content>
|
||||
<div className={`w-full min-h-[422px] space-y-5 flex flex-col justify-between text-primary-text`}>
|
||||
<div className='space-y-5'>
|
||||
<div className="w-full flex flex-col h-full space-y-5">
|
||||
<div className="bg-secondary-700 font-normal px-3 py-4 rounded-lg flex flex-col border border-secondary-500 w-full relative z-10">
|
||||
<SwapSummary></SwapSummary>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-secondary-700 font-normal px-3 py-6 rounded-lg flex flex-col border border-secondary-500 w-full relative z-10 divide-y-2 divide-secondary-500 divide-dashed">
|
||||
<div className='pb-4'>
|
||||
<div className='flex flex-col gap-2 items-center'>
|
||||
<div className='flex items-center'>
|
||||
<Gauge value={stepsProgressPercentage} size="small" showCheckmark={swap?.status === SwapStatus.Completed} />
|
||||
</div>
|
||||
<div className="flex-col text-center ">
|
||||
<span className="font-medium text-primary-text">
|
||||
{progressStatuses.generalStatus.title}
|
||||
</span>
|
||||
<span className='text-sm block space-x-1 text-secondary-text'>
|
||||
<span>{progressStatuses.generalStatus.subTitle ?? outputPendingDetails}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div></div>
|
||||
<div className='pt-4'>
|
||||
{
|
||||
swap?.status != SwapStatus.Cancelled && swap?.status != SwapStatus.Expired && currentSteps.find(x => x.status != null) &&
|
||||
<div className='flex flex-col h-full justify-center'>
|
||||
<Steps steps={currentSteps} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
([SwapStatus.Expired, SwapStatus.Cancelled, SwapStatus.UserTransferDelayed].includes(swap?.status)) &&
|
||||
<Failed />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Widget.Content>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const getProgressStatuses = (swap: SwapItem, swapStatus: SwapStatus): { stepStatuses: { [key in Progress]: ProgressStatus }, generalStatus: { title: string, subTitle: string | null } } => {
|
||||
let generalTitle = "Transfer in progress";
|
||||
let subtitle: string | null = "";
|
||||
//TODO might need to check stored wallet transaction statuses
|
||||
const swapInputTransaction = swap?.transactions?.find(t => t.type === TransactionType.Input)
|
||||
|
||||
const swapOutputTransaction = swap?.transactions?.find(t => t.type === TransactionType.Output);
|
||||
const swapRefuelTransaction = swap?.transactions?.find(t => t.type === TransactionType.Refuel);
|
||||
let inputIsCompleted = swapInputTransaction?.status == TransactionStatus.Completed && swapInputTransaction.confirmations >= swapInputTransaction.max_confirmations;
|
||||
if (!inputIsCompleted) {
|
||||
// Magic case, shows estimated time
|
||||
subtitle = null
|
||||
}
|
||||
let input_transfer = inputIsCompleted ? ProgressStatus.Complete : ProgressStatus.Current;
|
||||
|
||||
let output_transfer =
|
||||
(!swapOutputTransaction && inputIsCompleted) || swapOutputTransaction?.status == TransactionStatus.Pending ? ProgressStatus.Current
|
||||
: swapOutputTransaction?.status == TransactionStatus.Initiated || swapOutputTransaction?.status == TransactionStatus.Completed ? ProgressStatus.Complete
|
||||
: ProgressStatus.Upcoming;
|
||||
|
||||
let refuel_transfer =
|
||||
(swap.has_refuel && !swapRefuelTransaction) ? ProgressStatus.Upcoming
|
||||
: swapRefuelTransaction?.status == TransactionStatus.Pending ? ProgressStatus.Current
|
||||
: swapRefuelTransaction?.status == TransactionStatus.Initiated || swapRefuelTransaction?.status == TransactionStatus.Completed ? ProgressStatus.Complete
|
||||
: ProgressStatus.Removed;
|
||||
|
||||
if (swapStatus === SwapStatus.Failed) {
|
||||
output_transfer = output_transfer == ProgressStatus.Complete ? ProgressStatus.Complete : ProgressStatus.Failed;
|
||||
refuel_transfer = refuel_transfer !== ProgressStatus.Complete ? ProgressStatus.Removed : refuel_transfer;
|
||||
generalTitle = swap?.fail_reason == SwapFailReasons.RECEIVED_MORE_THAN_VALID_RANGE ? "Transfer on hold" : "Transfer failed";
|
||||
subtitle = "View instructions below"
|
||||
}
|
||||
|
||||
if (swapStatus === SwapStatus.UserTransferDelayed) {
|
||||
input_transfer = ProgressStatus.Removed;
|
||||
output_transfer = ProgressStatus.Removed;
|
||||
refuel_transfer = ProgressStatus.Removed;
|
||||
generalTitle = "Transfer delayed"
|
||||
subtitle = "View instructions below"
|
||||
}
|
||||
|
||||
if (swapStatus == SwapStatus.Completed) {
|
||||
generalTitle = "Transfer completed"
|
||||
subtitle = "Thanks for using Bridge"
|
||||
}
|
||||
if (swapStatus == SwapStatus.Cancelled) {
|
||||
generalTitle = "Transfer cancelled"
|
||||
subtitle = "..."
|
||||
}
|
||||
if (swapStatus == SwapStatus.Expired) {
|
||||
generalTitle = "Transfer expired"
|
||||
subtitle = "..."
|
||||
}
|
||||
return {
|
||||
stepStatuses: {
|
||||
"input_transfer": input_transfer,
|
||||
"output_transfer": output_transfer,
|
||||
"refuel": refuel_transfer,
|
||||
},
|
||||
generalStatus: {
|
||||
title: generalTitle,
|
||||
subTitle: subtitle
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default Processing;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { FC } from 'react'
|
||||
import { useSwapDataState } from '../../../../context/swap';
|
||||
import { useSettingsState } from '../../../../context/settings';
|
||||
import Processing from './Processing';
|
||||
|
||||
const Component: FC = () => {
|
||||
|
||||
const { swap } = useSwapDataState()
|
||||
const settings = useSettingsState()
|
||||
|
||||
return (
|
||||
<>
|
||||
{swap && <Processing settings={settings} swap={swap} />}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Component;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user