From 7d670532cc8bf5a532cd6837d86fe2eb2da92a96 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Wed, 6 Mar 2024 12:10:14 +0100
Subject: [PATCH 01/74] feat: Refactor container components and routing logic
---
src/components/AppShell.tsx | 161 ++++++++++++-------------------
src/components/Header.tsx | 24 ++---
src/components/Welcome.tsx | 25 +++--
src/containers/DefiContainer.tsx | 4 +-
src/containers/EarnContainer.tsx | 2 +-
src/containers/SwapContainer.tsx | 2 +-
6 files changed, 96 insertions(+), 122 deletions(-)
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index c3fbbb42..d8497735 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -2,13 +2,13 @@ import { IonApp, IonButton, IonRouterOutlet, setupIonicReact, IonText, IonChip,
import { StatusBar, Style } from '@capacitor/status-bar';
import { IonReactRouter } from '@ionic/react-router';
-import { Redirect, Route } from 'react-router-dom';
-import { useEffect, useRef, useState } from 'react';
+import { Redirect, Route, useHistory } from 'react-router-dom';
+import { useEffect, useRef, useState, lazy, Suspense } from 'react';
import { Welcome } from './Welcome';
-import { SwapContainer } from '@/containers/SwapContainer';
+// import { SwapContainer } from '@/containers/SwapContainer';
import { FiatContainer } from '@/containers/FiatContainer';
-import { DefiContainer } from '@/containers/DefiContainer';
-import { EarnContainer } from '@/containers/EarnContainer';
+// import { DefiContainer } from '@/containers/DefiContainer';
+// import { EarnContainer } from '@/containers/EarnContainer';
import { Header } from './Header';
import MenuSlide from './MenuSlide';
import { LoaderProvider } from '@/context/LoaderContext';
@@ -19,7 +19,7 @@ import { initializeWeb3 } from '@/store/effects/web3.effects';
import { getMagic } from '@/servcies/magic';
import Store from '@/store';
import { getWeb3State } from '@/store/selectors';
-
+import { useIonRouter, IonRoute } from '@ionic/react';
setupIonicReact({ mode: 'ios' });
@@ -32,19 +32,18 @@ window.matchMedia("(prefers-color-scheme: dark)").addListener(async (status) =>
} catch { }
});
+const SwapContainer = lazy(() => import('@/containers/SwapContainer'));
+const DefiContainer = lazy(() => import('@/containers/DefiContainer'));
+const EarnContainer = lazy(() => import('@/containers/EarnContainer'));
+
const AppShell = () => {
// get params from url `s=`
const urlParams = new URLSearchParams(window.location.search);
- let segment = urlParams.get("s") || "welcome";
+ const {pathname = '/swap'} = window.location;
+ let segment = pathname.split('/')[1]|| 'swap' // urlParams.get("s") || "swap";
const {walletAddress, isMagicWallet } = Store.useState(getWeb3State);
const [presentFiatWarning, dismissFiatWarning] = useIonAlert();
- // handle unsupported segment
- // if (segment && ['welcome', 'swap', 'fiat', 'defi', 'earn'].indexOf(segment) === -1) {
- // urlParams.delete('s');
- // segment = '';
- // // reload window with correct segment
- // window.location.href = `${window.location.origin}?${urlParams.toString()}`;
- // }
+ const isNotFound = (segment && ['swap', 'fiat', 'defi', 'earn'].indexOf(segment) === -1);
// use state to handle segment change
const [currentSegment, setSegment] = useState(segment);
const handleSegmentChange = async (e: any) => {
@@ -63,6 +62,7 @@ const AppShell = () => {
return;
};
setSegment(e.detail.value);
+ // router.push(`/${e.detail.value}`);
};
const contentRef = useRef(null);
const scrollToTop = () => {
@@ -70,97 +70,62 @@ const AppShell = () => {
contentRef.current.scrollToTop();
};
- const renderSwitch = (param: string) => {
- switch (param) {
- case "welcome":
- return ;
- case "swap":
- return ;
- case "fiat":
- return ;
- case "defi":
- return ;
- case "earn": {
- return
- }
- default:
- return currentSegment ?
- (
- <>
-
-
- {currentSegment.toUpperCase()}
-
- This feature is in development.
- Please check back later.
-
-
-
Coming soon
-
- >
- )
- : (<>>)
- }
- };
-
useEffect(()=> {
initializeWeb3();
}, []);
return (
-
+
- (<>
-
-
-
-
-
-
-
-
- {renderSwitch(currentSegment)}
- {currentSegment !== "welcome" && (
-
-
- {`HexaLite v${process.env.NEXT_PUBLIC_APP_VERSION} - ${process.env.NEXT_PUBLIC_APP_BUILD_DATE}`}
-
-
- )}
-
-
-
-
-
-
- >)} />
- } />
- } exact={true} />
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >} />
+ } />
+ } exact={true} />
+ <>
+
+ {!isNotFound && (
+
+ )}
+
+ Loading SwapContainer...}>
+ { currentSegment === 'swap' && }
+
+ Loading EarnContainer...}>
+ { currentSegment === 'earn' && }
+
+ Loading DefiContainer...}>
+ { currentSegment === 'defi' && }
+
+ Loading NotFoundPage...}>
+ { currentSegment === isNotFound && }
+
+
+
+ >} />
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 276de2a0..0393df46 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -12,6 +12,7 @@ import {
IonSegmentButton,
IonText,
IonToolbar,
+ useIonRouter,
} from "@ionic/react";
import {
ellipsisVerticalSharp,
@@ -26,6 +27,7 @@ import { PointsPopover } from "./PointsPopover";
import { useRef } from "react";
import { getWeb3State } from "@/store/selectors";
import Store from "@/store";
+import { Link } from "react-router-dom";
const styleLogo = {
// margin: '15px auto 20px',
@@ -49,11 +51,11 @@ const styleChip = {
export function Header({
currentSegment,
- scrollToTop,
+ // scrollToTop,
handleSegmentChange,
}: {
currentSegment: string;
- scrollToTop: () => void;
+ // scrollToTop: () => void;
handleSegmentChange: (e: { detail: { value: string } }) => void;
}) {
// define states
@@ -61,12 +63,13 @@ export function Header({
const [points, setPoints] = useState(null);
const [isPointsPopoverOpen, setIsPointsPopoverOpen] = useState(false);
const pointsPopoverRef = useRef(null);
+ const router = useIonRouter();
const openPopover = (e: any) => {
pointsPopoverRef.current!.event = e;
setIsPointsPopoverOpen(true);
};
useEffect(() => {
- scrollToTop();
+ // scrollToTop();
}, [currentSegment]);
// render component
@@ -76,24 +79,21 @@ export function Header({
{!currentSegment || currentSegment === "welcome" ? (
- <>>
+ <>{currentSegment}>
) : (
<>
-
- handleSegmentChange({ detail: { value: "welcome" } })
- }
+
+ }}>
-
+
{
+ console.log('>> router: ', router)
+ router.push(`/${e.detail.value}`)
if (e.detail.value === 'fiat-segment') {
handleSegmentChange({detail: {value: 'fiat'}});
return;
diff --git a/src/components/Welcome.tsx b/src/components/Welcome.tsx
index fdae1508..91653165 100644
--- a/src/components/Welcome.tsx
+++ b/src/components/Welcome.tsx
@@ -6,6 +6,7 @@ import {
IonImg,
IonRow,
IonText,
+ useIonRouter,
} from "@ionic/react";
import {
logoGithub,
@@ -24,6 +25,7 @@ export function Welcome({
handleSegmentChange: (e: { detail: { value: string } }) => void;
}) {
const { connectWallet } = Store.useState(getWeb3State);
+ const router = useIonRouter();
return (
@@ -57,9 +59,10 @@ export function Welcome({
size="large"
color="gradient"
style={{marginTop: '2rem'}}
- onClick={(e) =>
- handleSegmentChange({ detail: { value: "swap" } })
- }
+ onClick={(e) => {
+ router.push("swap");
+ handleSegmentChange({ detail: { value: "swap" } });
+ }}
>
Launch App
@@ -127,6 +130,7 @@ export function Welcome({
(e.target as HTMLElement).innerHTML = "Connecting...";
try {
await connectWallet();
+ router.push("fiat");
handleSegmentChange({ detail: { value: "fiat" } });
} catch (error) {
console.error("[ERROR] handleConnect:", error);
@@ -189,9 +193,10 @@ export function Welcome({
+ onClick={(e) =>{
+ router.push("defi");
handleSegmentChange({ detail: { value: "defi" } })
- }
+ }}
>
Start Deposit
@@ -243,9 +248,10 @@ export function Welcome({
+ onClick={(e) =>{
+ router.push("earn");
handleSegmentChange({ detail: { value: "earn" } })
- }
+ }}
>
Start Earning
@@ -664,9 +670,10 @@ export function Welcome({
className="ion-margin-top"
size="large"
color="gradient"
- onClick={(e) =>
+ onClick={(e) =>{
+ router.push("swap");
handleSegmentChange({ detail: { value: "swap" } })
- }
+ }}
>
Launch App
diff --git a/src/containers/DefiContainer.tsx b/src/containers/DefiContainer.tsx
index 2e265ea6..4629575f 100644
--- a/src/containers/DefiContainer.tsx
+++ b/src/containers/DefiContainer.tsx
@@ -34,11 +34,11 @@ export const minBaseTokenRemainingByNetwork: Record = {
[ChainId.arbitrum_one]: "0.0001",
};
-export const DefiContainer = ({
+export default function DefiContainer({
handleSegmentChange,
}: {
handleSegmentChange: (e: { detail: { value: string } }) => void;
-}) => {
+}) {
const { walletAddress } = Store.useState(getWeb3State);
const userSummaryAndIncentivesGroup = Store.useState(getUserSummaryAndIncentivesGroupState);
const poolGroups = Store.useState(getPoolGroupsState);
diff --git a/src/containers/EarnContainer.tsx b/src/containers/EarnContainer.tsx
index 6efbcd4b..cc0459c0 100644
--- a/src/containers/EarnContainer.tsx
+++ b/src/containers/EarnContainer.tsx
@@ -31,7 +31,7 @@ const LSTInfo = () => {
);
}
-export function EarnContainer() {
+export default function EarnContainer() {
return (
diff --git a/src/containers/SwapContainer.tsx b/src/containers/SwapContainer.tsx
index a46783c1..c780fdf8 100644
--- a/src/containers/SwapContainer.tsx
+++ b/src/containers/SwapContainer.tsx
@@ -26,7 +26,7 @@ import { PointsData, addAddressPoints } from "@/servcies/datas.service";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
-export function SwapContainer() {
+export default function SwapContainer() {
const {
web3Provider,
currentNetwork,
From c5d02a557e13b6d9bcba7bd990416a2dfa5215d7 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Wed, 6 Mar 2024 12:48:22 +0100
Subject: [PATCH 02/74] refactor: Add isMobilePWADevice check for mobile UI
---
src/components/AppShell.tsx | 117 ++++++++++++++++++++----------------
1 file changed, 64 insertions(+), 53 deletions(-)
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index d8497735..b97ce693 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -20,6 +20,7 @@ import { getMagic } from '@/servcies/magic';
import Store from '@/store';
import { getWeb3State } from '@/store/selectors';
import { useIonRouter, IonRoute } from '@ionic/react';
+import { isPlatform } from '@ionic/core';
setupIonicReact({ mode: 'ios' });
@@ -36,9 +37,10 @@ const SwapContainer = lazy(() => import('@/containers/SwapContainer'));
const DefiContainer = lazy(() => import('@/containers/DefiContainer'));
const EarnContainer = lazy(() => import('@/containers/EarnContainer'));
+const isMobilePWADevice = Boolean(isPlatform('pwa') && !isPlatform('desktop'));
+
const AppShell = () => {
// get params from url `s=`
- const urlParams = new URLSearchParams(window.location.search);
const {pathname = '/swap'} = window.location;
let segment = pathname.split('/')[1]|| 'swap' // urlParams.get("s") || "swap";
const {walletAddress, isMagicWallet } = Store.useState(getWeb3State);
@@ -76,58 +78,67 @@ const AppShell = () => {
return (
-
-
- <>
-
-
-
-
-
-
-
-
-
-
-
- >} />
- } />
- } exact={true} />
- <>
-
- {!isNotFound && (
-
- )}
-
- Loading SwapContainer...}>
- { currentSegment === 'swap' && }
-
- Loading EarnContainer...}>
- { currentSegment === 'earn' && }
-
- Loading DefiContainer...}>
- { currentSegment === 'defi' && }
-
- Loading NotFoundPage...}>
- { currentSegment === isNotFound && }
-
-
-
- >} />
-
-
+ {!isMobilePWADevice && (
+
+
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >} />
+ } />
+ } exact={true} />
+ <>
+
+ {!isNotFound && (
+
+ )}
+
+ Loading SwapContainer...}>
+ { currentSegment === 'swap' && }
+
+ Loading EarnContainer...}>
+ { currentSegment === 'earn' && }
+
+ Loading DefiContainer...}>
+ { currentSegment === 'defi' && }
+
+ Loading NotFoundPage...}>
+ { currentSegment === isNotFound && }
+
+
+
+ >} />
+
+
+ )}
+ {isMobilePWADevice && (
+
+
+ {/* Here use mobile UI */}
+
+
+ )}
);
From 13ddb47ccaaee7e0e75859238c0b942fbb4cb612 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Wed, 6 Mar 2024 13:12:34 +0100
Subject: [PATCH 03/74] refactor: add redirect to different routes based on
platform
---
src/components/AppShell.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index b97ce693..1dd3cb92 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -105,7 +105,7 @@ const AppShell = () => {
>} />
} />
- } exact={true} />
+ } exact={true} />
<>
{!isNotFound && (
@@ -136,6 +136,7 @@ const AppShell = () => {
{/* Here use mobile UI */}
+ hi
)}
From da84fe3ececc6c6a3c6b7d7eef004550272de951 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Sat, 9 Mar 2024 09:16:39 +0100
Subject: [PATCH 04/74] refactor: build wallet page and modals
---
package-lock.json | 15 +-
package.json | 2 +
public/assets/icons/bank.svg | 3 +
src/components/AppShell.tsx | 272 +++++++----
src/components/mobile/MobileDepositModal.tsx | 121 +++++
src/components/mobile/MobileEarnModal.tsx | 168 +++++++
src/components/mobile/MobileSwapModal.tsx | 186 ++++++++
.../mobile/MobileTokenDetailModal.tsx | 51 ++
src/components/mobile/MobileTransferModal.tsx | 21 +
.../mobile/WalletComponent.module.css | 12 +
src/components/mobile/WalletComponent.tsx | 434 ++++++++++++++++++
src/components/mobile/WelcomeComponent.tsx | 38 ++
src/servcies/ankr.service.ts | 2 +-
src/servcies/lifi.service.ts | 2 +-
src/servcies/qrcode.service.ts | 12 +
src/styles/global.scss | 18 +
16 files changed, 1253 insertions(+), 104 deletions(-)
create mode 100644 public/assets/icons/bank.svg
create mode 100644 src/components/mobile/MobileDepositModal.tsx
create mode 100644 src/components/mobile/MobileEarnModal.tsx
create mode 100644 src/components/mobile/MobileSwapModal.tsx
create mode 100644 src/components/mobile/MobileTokenDetailModal.tsx
create mode 100644 src/components/mobile/MobileTransferModal.tsx
create mode 100644 src/components/mobile/WalletComponent.module.css
create mode 100644 src/components/mobile/WalletComponent.tsx
create mode 100644 src/components/mobile/WelcomeComponent.tsx
create mode 100644 src/servcies/qrcode.service.ts
diff --git a/package-lock.json b/package-lock.json
index 51371d72..39a53d9a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "hexa-lite",
- "version": "1.0.5",
+ "version": "1.0.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hexa-lite",
- "version": "1.0.5",
+ "version": "1.0.8",
"dependencies": {
"@0xsquid/widget": "^1.6.23",
"@aave/contract-helpers": "^1.21.1",
@@ -46,6 +46,7 @@
"magic-sdk": "^21.4.0",
"next": "14.0.3",
"pullstate": "1.25",
+ "qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^5.3.4",
@@ -69,6 +70,7 @@
"@types/cors": "^2.8.17",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.36",
+ "@types/qrcode": "^1.5.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"@types/swagger-ui-react": "^4.18.3",
@@ -9357,6 +9359,15 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
+ "node_modules/@types/qrcode": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
+ "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/ramda": {
"version": "0.29.9",
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.9.tgz",
diff --git a/package.json b/package.json
index f25860ab..c075bd2a 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"magic-sdk": "^21.4.0",
"next": "14.0.3",
"pullstate": "1.25",
+ "qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^5.3.4",
@@ -75,6 +76,7 @@
"@types/cors": "^2.8.17",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.36",
+ "@types/qrcode": "^1.5.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"@types/swagger-ui-react": "^4.18.3",
diff --git a/public/assets/icons/bank.svg b/public/assets/icons/bank.svg
new file mode 100644
index 00000000..f8e80613
--- /dev/null
+++ b/public/assets/icons/bank.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index 1dd3cb92..66bfdab5 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -1,68 +1,93 @@
-import { IonApp, IonButton, IonRouterOutlet, setupIonicReact, IonText, IonChip, IonContent, IonGrid, IonRow, IonCol, IonPage, useIonModal, IonIcon, useIonAlert } from '@ionic/react';
-import { StatusBar, Style } from '@capacitor/status-bar';
+import {
+ IonApp,
+ IonButton,
+ IonRouterOutlet,
+ setupIonicReact,
+ IonText,
+ IonChip,
+ IonContent,
+ IonGrid,
+ IonRow,
+ IonCol,
+ IonPage,
+ useIonModal,
+ IonIcon,
+ useIonAlert,
+ IonImg,
+} from "@ionic/react";
+import { StatusBar, Style } from "@capacitor/status-bar";
-import { IonReactRouter } from '@ionic/react-router';
-import { Redirect, Route, useHistory } from 'react-router-dom';
-import { useEffect, useRef, useState, lazy, Suspense } from 'react';
-import { Welcome } from './Welcome';
+import { IonReactRouter } from "@ionic/react-router";
+import { Redirect, Route, useHistory } from "react-router-dom";
+import { useEffect, useRef, useState, lazy, Suspense } from "react";
+import { Welcome } from "./Welcome";
// import { SwapContainer } from '@/containers/SwapContainer';
-import { FiatContainer } from '@/containers/FiatContainer';
+// import { FiatContainer } from "@/containers/FiatContainer";
// import { DefiContainer } from '@/containers/DefiContainer';
// import { EarnContainer } from '@/containers/EarnContainer';
-import { Header } from './Header';
-import MenuSlide from './MenuSlide';
-import { LoaderProvider } from '@/context/LoaderContext';
-import { Leaderboard } from '@/containers/LeaderboardContainer';
-import { NotFoundPage } from '@/containers/NotFoundPage';
-import PwaInstall from './PwaInstall';
-import { initializeWeb3 } from '@/store/effects/web3.effects';
-import { getMagic } from '@/servcies/magic';
-import Store from '@/store';
-import { getWeb3State } from '@/store/selectors';
-import { useIonRouter, IonRoute } from '@ionic/react';
-import { isPlatform } from '@ionic/core';
+import { Header } from "./Header";
+import MenuSlide from "./MenuSlide";
+import { LoaderProvider } from "@/context/LoaderContext";
+import { Leaderboard } from "@/containers/LeaderboardContainer";
+import { NotFoundPage } from "@/containers/NotFoundPage";
+import PwaInstall from "./PwaInstall";
+import { initializeWeb3 } from "@/store/effects/web3.effects";
+import { getMagic } from "@/servcies/magic";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import { IonRoute } from "@ionic/react";
+import { isPlatform } from "@ionic/core";
-setupIonicReact({ mode: 'ios' });
+setupIonicReact({ mode: "ios" });
-window.matchMedia("(prefers-color-scheme: dark)").addListener(async (status) => {
- console.log(`[INFO] Dark mode is ${status.matches ? 'enabled' : 'disabled'}`);
- try {
- await StatusBar.setStyle({
- style: status.matches ? Style.Dark : Style.Light,
- });
- } catch { }
-});
+window
+ .matchMedia("(prefers-color-scheme: dark)")
+ .addListener(async (status) => {
+ console.log(
+ `[INFO] Dark mode is ${status.matches ? "enabled" : "disabled"}`
+ );
+ try {
+ await StatusBar.setStyle({
+ style: status.matches ? Style.Dark : Style.Light,
+ });
+ } catch {}
+ });
-const SwapContainer = lazy(() => import('@/containers/SwapContainer'));
-const DefiContainer = lazy(() => import('@/containers/DefiContainer'));
-const EarnContainer = lazy(() => import('@/containers/EarnContainer'));
+const SwapContainer = lazy(() => import("@/containers/SwapContainer"));
+const DefiContainer = lazy(() => import("@/containers/DefiContainer"));
+const EarnContainer = lazy(() => import("@/containers/EarnContainer"));
+const MobileWalletComponent = lazy(() => import("@/components/mobile/WalletComponent"));
+const MobileWelcomeComponent = lazy(() => import("@/components/mobile/WelcomeComponent"));
-const isMobilePWADevice = Boolean(isPlatform('pwa') && !isPlatform('desktop'));
+const isMobilePWADevice = true //Boolean(isPlatform('pwa') && !isPlatform('desktop'));
const AppShell = () => {
// get params from url `s=`
- const {pathname = '/swap'} = window.location;
- let segment = pathname.split('/')[1]|| 'swap' // urlParams.get("s") || "swap";
- const {walletAddress, isMagicWallet } = Store.useState(getWeb3State);
+ const { pathname = "/swap" } = window.location;
+ let segment = pathname.split("/")[1] || "swap"; // urlParams.get("s") || "swap";
+ const { walletAddress, isMagicWallet } = Store.useState(getWeb3State);
const [presentFiatWarning, dismissFiatWarning] = useIonAlert();
- const isNotFound = (segment && ['swap', 'fiat', 'defi', 'earn'].indexOf(segment) === -1);
+
+ const isNotFound =
+ segment && ["swap", "fiat", "defi", "earn"].indexOf(segment) === -1;
// use state to handle segment change
const [currentSegment, setSegment] = useState(segment);
const handleSegmentChange = async (e: any) => {
- if (e.detail.value === 'fiat'){
- if (walletAddress && walletAddress !== '' && isMagicWallet) {
+ if (e.detail.value === "fiat") {
+ if (walletAddress && walletAddress !== "" && isMagicWallet) {
const magic = await getMagic();
magic.wallet.showOnRamp();
} else {
await presentFiatWarning({
- header: 'Information',
- message: 'Connect with e-mail or social login to enable buy crypto with fiat.',
- buttons: ['OK'],
- cssClass: 'modalAlert'
+ header: "Information",
+ message:
+ "Connect with e-mail or social login to enable buy crypto with fiat.",
+ buttons: ["OK"],
+ cssClass: "modalAlert",
});
- };
+ }
return;
- };
+ }
setSegment(e.detail.value);
// router.push(`/${e.detail.value}`);
};
@@ -72,73 +97,120 @@ const AppShell = () => {
contentRef.current.scrollToTop();
};
- useEffect(()=> {
+ useEffect(() => {
initializeWeb3();
}, []);
-
+
return (
{!isMobilePWADevice && (
-
+
- <>
-
-
-
-
-
-
-
-
-
-
-
- >} />
+ (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ />
} />
- } exact={true} />
- <>
-
- {!isNotFound && (
-
- )}
-
- Loading SwapContainer...}>
- { currentSegment === 'swap' && }
-
- Loading EarnContainer...}>
- { currentSegment === 'earn' && }
-
- Loading DefiContainer...}>
- { currentSegment === 'defi' && }
-
- Loading NotFoundPage...}>
- { currentSegment === isNotFound && }
-
-
-
- >} />
+ (
+
+ )}
+ exact={true}
+ />
+ (
+ <>
+
+ {!isNotFound && (
+
+ )}
+
+ Loading SwapContainer...}>
+ {currentSegment === "swap" && }
+
+ Loading EarnContainer...}>
+ {currentSegment === "earn" && }
+
+ Loading DefiContainer...}>
+ {currentSegment === "defi" && (
+
+ )}
+
+ Loading NotFoundPage...}>
+ {currentSegment === isNotFound && }
+
+
+
+ >
+ )}
+ />
)}
+
+ {/* Here use mobile UI */}
{isMobilePWADevice && (
-
-
- {/* Here use mobile UI */}
- hi
-
-
+
+
+ !walletAddress ?
+ (
+ Loading MobileWelcomeComponent...}>
+
+
+ ) :
+ (
+ Loading MobileWalletComponent...}>
+
+
+ )
+ }
+ />
+ (
+
+ )}
+ exact={true}
+ />
+
+
)}
diff --git a/src/components/mobile/MobileDepositModal.tsx b/src/components/mobile/MobileDepositModal.tsx
new file mode 100644
index 00000000..5c9a78bc
--- /dev/null
+++ b/src/components/mobile/MobileDepositModal.tsx
@@ -0,0 +1,121 @@
+import { CHAIN_AVAILABLES } from "@/constants/chains";
+import { getQrcodeAsSVG } from "@/servcies/qrcode.service";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import {
+ IonButton,
+ IonCol,
+ IonContent,
+ IonFooter,
+ IonGrid,
+ IonHeader,
+ IonIcon,
+ IonRow,
+ IonText,
+ IonToolbar,
+} from "@ionic/react";
+import { useEffect, useState } from "react";
+import { scan } from 'ionicons/icons';
+
+export const MobileDepositModal = () => {
+ const {
+ web3Provider,
+ currentNetwork,
+ walletAddress,
+ connectWallet,
+ disconnectWallet,
+ switchNetwork,
+ } = Store.useState(getWeb3State);
+ const [qrCodeSVG, setQrCodeSVG] = useState(null);
+
+ useEffect(() => {
+ if (!walletAddress) {
+ return;
+ }
+ getQrcodeAsSVG(walletAddress).then((url) => {
+ // convet string to SVG
+ const svg = new DOMParser().parseFromString(
+ url as string,
+ "image/svg+xml"
+ );
+ console.log(svg.documentElement);
+ setQrCodeSVG(svg.documentElement as unknown as SVGElement);
+ });
+ }, [walletAddress]);
+
+ return (
+ <>
+
+
+
+
+
+
+ Deposit
+
+
+
+
+
+
+
+
+
+
+ {walletAddress}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You can send token to all this following networks that
+ are available to this address
+
+
+
+
+ {CHAIN_AVAILABLES.filter((c) => c.type === "evm").map(
+ (chain, index) => (
+
+
+
+ )
+ )}
+
+
+
+
+ {/*
+
+
+
+ Scan QR Code
+
+
+ */}
+
+
+ >
+ );
+};
diff --git a/src/components/mobile/MobileEarnModal.tsx b/src/components/mobile/MobileEarnModal.tsx
new file mode 100644
index 00000000..ca18da55
--- /dev/null
+++ b/src/components/mobile/MobileEarnModal.tsx
@@ -0,0 +1,168 @@
+import {
+ IonCol,
+ IonContent,
+ IonGrid,
+ IonHeader,
+ IonRow,
+ IonSegment,
+ IonSegmentButton,
+ IonSkeletonText,
+ IonText,
+ IonToolbar,
+} from "@ionic/react";
+import { useEffect, useMemo, useState } from "react";
+import { MarketList } from "../MarketsList";
+import {
+ initializePools,
+ initializeUserSummary,
+} from "@/store/effects/pools.effect";
+import Store from "@/store";
+import {
+ getPoolGroupsState,
+ getUserSummaryAndIncentivesGroupState,
+ getWeb3State,
+} from "@/store/selectors";
+import { patchPoolsState } from "@/store/actions";
+import { CHAIN_AVAILABLES } from "@/constants/chains";
+import { getReadableValue } from "@/utils/getReadableValue";
+
+export const MobileEarnModal = () => {
+ const [segment, setSegment] = useState("loan");
+ const { walletAddress } = Store.useState(getWeb3State);
+ const userSummaryAndIncentivesGroup = Store.useState(
+ getUserSummaryAndIncentivesGroupState
+ );
+ const poolGroups = Store.useState(getPoolGroupsState);
+
+ const totalTVL = useMemo(() => {
+ return poolGroups
+ .flatMap((g) => g.pools)
+ .reduce(
+ (prev, current) => prev + Number(current.totalLiquidityUSD || 0),
+ 0
+ );
+ }, [poolGroups]);
+
+ useEffect(() => {
+ if (poolGroups.length > 0 && totalTVL > 0) {
+ return;
+ }
+ initializePools();
+ }, []);
+
+ useEffect(() => {
+ if (!walletAddress) {
+ patchPoolsState({ userSummaryAndIncentivesGroup: null });
+ return;
+ }
+ if (!userSummaryAndIncentivesGroup && walletAddress) {
+ initializeUserSummary(walletAddress);
+ }
+ }, [walletAddress, userSummaryAndIncentivesGroup]);
+
+ return (
+ <>
+
+
+
+ setSegment(() => "loan")}
+ >
+ Loan market
+
+ setSegment(() => "earn")}
+ >
+ Earn
+
+
+
+
+
+ {segment === "loan" && (
+
+
+
+
+ Available Markets
+
+
+
+
+ Connect to DeFi liquidity protocols and access to{" "}
+ {poolGroups.length > 0 ? (
+ poolGroups.length
+ ) : (
+
+ )}{" "}
+ open markets across{" "}
+ {
+ CHAIN_AVAILABLES.filter(
+ (chain) =>
+ chain.type === "evm" || chain.type === "solana"
+ ).length
+ }{" "}
+ networks, borrow assets using your crypto as collateral
+ and earn interest without any restrictions or censorship
+ by providing liquidity over a
+
+
+ {}
+ {(totalTVL || 0) > 0 ? (
+ "$" + getReadableValue(totalTVL || 0)
+ ) : (
+
+ )}{" "}
+ TVL
+
+
+
+
+
+
+
+ {}}
+ />
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/src/components/mobile/MobileSwapModal.tsx b/src/components/mobile/MobileSwapModal.tsx
new file mode 100644
index 00000000..bc22b0a5
--- /dev/null
+++ b/src/components/mobile/MobileSwapModal.tsx
@@ -0,0 +1,186 @@
+import {
+ IonCol,
+ IonContent,
+ IonGrid,
+ IonRow,
+ IonText,
+ useIonToast,
+} from "@ionic/react";
+import {
+ HiddenUI,
+ RouteExecutionUpdate,
+ WidgetConfig,
+ WidgetEvent,
+ useWidgetEvents,
+} from "@lifi/widget";
+import type { Route } from "@lifi/sdk";
+import { useEffect } from "react";
+import { PointsData, addAddressPoints } from "@/servcies/datas.service";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import { CHAIN_DEFAULT } from "@/constants/chains";
+import { ethers } from "ethers";
+import { LiFiWidgetDynamic } from "../LiFiWidgetDynamic";
+import { useLoader } from "@/context/LoaderContext";
+import { LIFI_CONFIG } from "../../servcies/lifi.service";
+import { IAsset } from "@/interfaces/asset.interface";
+
+export const MobileSwapModal = (props?: {
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+}) => {
+ const {
+ web3Provider,
+ currentNetwork,
+ walletAddress,
+ connectWallet,
+ disconnectWallet,
+ switchNetwork,
+ } = Store.useState(getWeb3State);
+ const widgetEvents = useWidgetEvents();
+ const { display: displayLoader, hide: hideLoader } = useLoader();
+ const toastContext = useIonToast();
+ const presentToast = toastContext[0];
+ const dismissToast = toastContext[1];
+
+ useEffect(() => {
+ const onRouteExecutionStarted = (route: Route) => {
+ console.log("[INFO] onRouteExecutionStarted fired.");
+ };
+ const onRouteExecutionCompleted = async (route: Route) => {
+ console.log("[INFO] onRouteExecutionCompleted fired.", route);
+ const data: PointsData = {
+ route,
+ actionType: "swap",
+ };
+ if (!walletAddress) {
+ return;
+ }
+ await addAddressPoints(walletAddress, data);
+ };
+ const onRouteExecutionFailed = (update: RouteExecutionUpdate) => {
+ console.log("[INFO] onRouteExecutionFailed fired.", update);
+ };
+
+ widgetEvents.on(WidgetEvent.RouteExecutionStarted, onRouteExecutionStarted);
+ widgetEvents.on(
+ WidgetEvent.RouteExecutionCompleted,
+ onRouteExecutionCompleted
+ );
+ widgetEvents.on(WidgetEvent.RouteExecutionFailed, onRouteExecutionFailed);
+
+ return () => widgetEvents.all.clear();
+ }, [widgetEvents]);
+
+ const signer =
+ web3Provider instanceof ethers.providers.Web3Provider && walletAddress
+ ? web3Provider?.getSigner()
+ : undefined;
+ // load environment config
+ const widgetConfig: WidgetConfig = {
+ ...LIFI_CONFIG,
+ containerStyle: {
+ border: `0px solid rgba(var(--ion-color-primary-rgb), 0.4);`,
+ },
+ hiddenUI: [
+ ...(LIFI_CONFIG?.hiddenUI as any[]),
+ HiddenUI.History,
+ HiddenUI.WalletMenu,
+ // HiddenUI.DrawerButton,
+ // HiddenUI.DrawerCloseButton
+ ],
+ fee: 0, // set fee to 0 for main swap feature
+ walletManagement: {
+ connect: async () => {
+ try {
+ await displayLoader();
+ await connectWallet();
+ if (!(web3Provider instanceof ethers.providers.Web3Provider)) {
+ throw new Error(
+ "[ERROR] Only support ethers.providers.Web3Provider"
+ );
+ }
+ const signer = web3Provider?.getSigner();
+ console.log("signer", signer);
+ if (!signer) {
+ throw new Error("Signer not found");
+ }
+ // return signer instance from JsonRpcSigner
+ hideLoader();
+ return signer;
+ } catch (error: any) {
+ // Log any errors that occur during the connection process
+ hideLoader();
+ await presentToast({
+ message: `[ERROR] Connect Failed with reason: ${
+ error?.message || error
+ }`,
+ color: "danger",
+ buttons: [
+ {
+ text: "x",
+ role: "cancel",
+ handler: () => {
+ dismissToast();
+ },
+ },
+ ],
+ });
+ throw new Error("handleConnect:" + error?.message);
+ }
+ },
+ disconnect: async () => {
+ try {
+ displayLoader();
+ await disconnectWallet();
+ hideLoader();
+ } catch (error: any) {
+ // Log any errors that occur during the disconnection process
+ console.log("handleDisconnect:", error);
+ hideLoader();
+ await presentToast({
+ message: `[ERROR] Disconnect Failed with reason: ${
+ error?.message || error
+ }`,
+ color: "danger",
+ buttons: [
+ {
+ text: "x",
+ role: "cancel",
+ handler: () => {
+ dismissToast();
+ },
+ },
+ ],
+ });
+ }
+ },
+ signer,
+ },
+ // set source chain to Polygon
+ fromChain: props?.assets?.[0]?.chain?.id || CHAIN_DEFAULT.id,
+ // set destination chain to Optimism
+ toChain: currentNetwork || CHAIN_DEFAULT.id,
+ // set source token to ETH (Ethereum)
+ fromToken:
+ props?.assets?.[0]?.contractAddress ||
+ "0x0000000000000000000000000000000000000000",
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/mobile/MobileTokenDetailModal.tsx b/src/components/mobile/MobileTokenDetailModal.tsx
new file mode 100644
index 00000000..095f64ed
--- /dev/null
+++ b/src/components/mobile/MobileTokenDetailModal.tsx
@@ -0,0 +1,51 @@
+import { IAsset } from "@/interfaces/asset.interface";
+import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
+import { IonAvatar, IonCol, IonContent, IonGrid, IonRow, IonText } from "@ionic/react"
+
+export const MobileTokenDetailModal = (props: {
+ name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[];
+ })=> {
+ return (
+
+
+
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${props.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {props.symbol}
+
+
+ {props.name}
+
+
+
+
+ $ {props.balanceUsd.toFixed(2)}
+ {props.balance.toFixed(6)}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/mobile/MobileTransferModal.tsx b/src/components/mobile/MobileTransferModal.tsx
new file mode 100644
index 00000000..8b171ab3
--- /dev/null
+++ b/src/components/mobile/MobileTransferModal.tsx
@@ -0,0 +1,21 @@
+import { IAsset } from "@/interfaces/asset.interface";
+import { IonCol, IonContent, IonGrid, IonRow, IonText } from "@ionic/react";
+
+export const MobileTransferModal = (props: {
+ name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[];
+ }) => {
+ return (
+
+
+
+
+
+ Send token
+
+
+ xxx
+
+
+
+ );
+};
diff --git a/src/components/mobile/WalletComponent.module.css b/src/components/mobile/WalletComponent.module.css
new file mode 100644
index 00000000..450eebe6
--- /dev/null
+++ b/src/components/mobile/WalletComponent.module.css
@@ -0,0 +1,12 @@
+.header {
+ /* background: transparent; */
+}
+
+.stickyWrapper {
+ position: relative;
+
+}
+.stickyElement {
+ position: sticky;
+ top: 0;
+}
\ No newline at end of file
diff --git a/src/components/mobile/WalletComponent.tsx b/src/components/mobile/WalletComponent.tsx
new file mode 100644
index 00000000..232a87eb
--- /dev/null
+++ b/src/components/mobile/WalletComponent.tsx
@@ -0,0 +1,434 @@
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import { getReadableValue } from "@/utils/getReadableValue";
+import {
+ IonAvatar,
+ IonButton,
+ IonButtons,
+ IonCard,
+ IonCardContent,
+ IonCol,
+ IonContent,
+ IonFab,
+ IonFabButton,
+ IonGrid,
+ IonHeader,
+ IonIcon,
+ IonItem,
+ IonItemOption,
+ IonItemOptions,
+ IonItemSliding,
+ IonLabel,
+ IonList,
+ IonPage,
+ IonRow,
+ IonSearchbar,
+ IonText,
+ IonTitle,
+ IonToolbar,
+ ModalOptions,
+ useIonAlert,
+ useIonModal,
+ useIonRouter,
+} from "@ionic/react";
+import { paperPlane, download, repeat, card } from "ionicons/icons";
+import { ComponentRef, useMemo, useState } from "react";
+import styleRule from "./WalletComponent.module.css";
+import { IAsset } from "@/interfaces/asset.interface";
+import { MobileDepositModal } from "./MobileDepositModal";
+import { HookOverlayOptions } from "@ionic/react/dist/types/hooks/HookOverlayOptions";
+import { MobileTransferModal } from "./MobileTransferModal";
+import { getMagic } from "@/servcies/magic";
+import { MobileSwapModal } from "./MobileSwapModal";
+import { MobileEarnModal } from "./MobileEarnModal";
+import { MobileTokenDetailModal } from "./MobileTokenDetailModal";
+import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
+
+const style = {
+ fullHeight: {
+ height: "100%",
+ },
+ fab: {
+ display: "contents",
+ },
+};
+
+export default function WalletComponent() {
+ const { walletAddress, isMagicWallet, assets } = Store.useState(getWeb3State);
+ const [presentAlert, dismissAlert] = useIonAlert();
+ const [filterBy, setFilterBy] = useState(null);
+ const [selectedTokenDetail, setSelectedTokenDetail] = useState<{
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+ } | null>(null);
+ const [presentTransfer, dismissTransfer] = useIonModal(MobileTransferModal, {
+ ...selectedTokenDetail,
+ });
+ const [presentDeposit, dismissDeposit] = useIonModal(MobileDepositModal, {
+ ...selectedTokenDetail,
+ });
+ const [presentSwap, dismissSwap] = useIonModal(MobileSwapModal, {
+ ...selectedTokenDetail,
+ });
+ const [presentTokenDetail, dismissTokenDetail] = useIonModal(
+ MobileTokenDetailModal,
+ {
+ ...selectedTokenDetail,
+ }
+ );
+ const [presentEarn, dismissEarn] = useIonModal(MobileEarnModal, {});
+
+ const modalOpts: Omit &
+ HookOverlayOptions = {
+ initialBreakpoint: 0.98,
+ breakpoints: [0, 0.98],
+ };
+ // const assets: any[] = [];
+ const router = useIonRouter();
+ const balance = useMemo(() => {
+ if (!assets) {
+ return 0;
+ }
+ return assets.reduce((acc, asset) => {
+ return acc + asset.balanceUsd;
+ }, 0);
+ }, [assets]);
+
+ const assetGroup = useMemo(
+ () =>
+ assets.reduce((acc, asset) => {
+ // check existing asset symbol
+ const index = acc.findIndex((a) => a.symbol === asset.symbol);
+ if (index !== -1) {
+ acc[index].balance += asset.balance;
+ acc[index].balanceUsd += asset.balanceUsd;
+ acc[index].assets.push(asset);
+ } else {
+ acc.push({
+ name: asset.name,
+ symbol: asset.symbol,
+ priceUsd: asset.priceUsd,
+ thumbnail: asset.thumbnail,
+ balance: asset.balance,
+ balanceUsd: asset.balanceUsd,
+ assets: [asset],
+ });
+ }
+ return acc;
+ }, [] as { name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[] }[]),
+ [assets]
+ );
+
+ return (
+
+
+
+
+ Wallet
+
+ $ {balance.toFixed(2)}
+
+
+
+
+
+
+
+
+
+
+
+
+ Wallet
+
+ $ {getReadableValue(balance)}
+
+
+
+
+
+
+
+
+ presentTransfer(modalOpts)}
+ >
+
+
+
+
+
+
+ presentDeposit(modalOpts)}
+ >
+
+
+
+
+
+
+ presentSwap(modalOpts)}
+ >
+
+
+
+
+
+
+ presentEarn(modalOpts)}
+ >
+
+
+
+
+
+ {assetGroup.length > 0 && (
+
+
+
+ {
+ console.log(event);
+ setFilterBy(event.detail.value || null);
+ }}
+ >
+
+
+
+ )}
+
+
+
+
+
+ {balance <= 0 && (
+
+
+ {
+ if (
+ walletAddress &&
+ walletAddress !== "" &&
+ isMagicWallet
+ ) {
+ const magic = await getMagic();
+ magic.wallet.showOnRamp();
+ } else {
+ await presentAlert({
+ header: "Information",
+ message:
+ "Connect with e-mail or social login to enable buy crypto with fiat.",
+ buttons: ["OK"],
+ cssClass: "modalAlert",
+ });
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+ Buy crypto
+
+ You have to get ETH to use your wallet. Buy with
+ credit card or with Apple Pay
+
+
+
+
+
+
+
+
+ presentDeposit(modalOpts)}
+ >
+
+
+
+
+
+
+
+
+ Deposit assets
+
+ You have to get ETH to use your wallet. Buy with
+ credit card or with Apple Pay
+
+
+
+
+
+
+
+
+
+ )}
+
+ {balance > 0 && (
+
+
+
+ {assetGroup
+ .filter((asset) =>
+ filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(filterBy.toLowerCase())
+ : true
+ )
+ .sort((a, b) => (a.balanceUsd > b.balanceUsd ? -1 : 1))
+ .map((asset, index) => (
+
+ {
+ setSelectedTokenDetail(() => asset);
+ presentTokenDetail({
+ ...modalOpts,
+ onDidDismiss: () => setSelectedTokenDetail(null),
+ });
+ }}
+ >
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+
+ {asset.symbol}
+
+
+
+ {asset.name}
+
+
+
+
+
+ $ {asset.balanceUsd.toFixed(2)}
+
+
+ {asset.balance.toFixed(6)}
+
+
+
+
+
+ {
+ setSelectedTokenDetail(() => asset);
+ presentTransfer({
+ ...modalOpts,
+ onDidDismiss: () =>
+ setSelectedTokenDetail(null),
+ });
+ }}
+ >
+
+
+ {
+ setSelectedTokenDetail(() => asset);
+ presentSwap({
+ ...modalOpts,
+ onDidDismiss: () =>
+ setSelectedTokenDetail(null),
+ });
+ }}
+ >
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/mobile/WelcomeComponent.tsx b/src/components/mobile/WelcomeComponent.tsx
new file mode 100644
index 00000000..ab11c17a
--- /dev/null
+++ b/src/components/mobile/WelcomeComponent.tsx
@@ -0,0 +1,38 @@
+import { IonCol, IonContent, IonGrid, IonImg, IonRow, IonText, useIonRouter } from "@ionic/react";
+import { IonPage, } from '@ionic/react';
+import ConnectButton from "../ConnectButton";
+
+export default function MobileWelcomeComponent() {
+ const router = useIonRouter();
+ return (
+
+
+
+
+
+
+
+
+ Hexa Lite
+
+ Build your wealth with cryptoassets
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/servcies/ankr.service.ts b/src/servcies/ankr.service.ts
index a1e15b5f..d7e992e0 100644
--- a/src/servcies/ankr.service.ts
+++ b/src/servcies/ankr.service.ts
@@ -67,7 +67,7 @@ export const getTokensBalances = async (chainIds: number[], address: string) =>
params: {
blockchain,
walletAddress: address,
- onlyWhitelisted: false,
+ onlyWhitelisted: true,
},
id: 1,
}),
diff --git a/src/servcies/lifi.service.ts b/src/servcies/lifi.service.ts
index 13afe431..72e70c99 100644
--- a/src/servcies/lifi.service.ts
+++ b/src/servcies/lifi.service.ts
@@ -802,7 +802,7 @@ export const swapWithLiFi = async (
export const LIFI_CONFIG = Object.freeze({
// integrator: "cra-example",
integrator: process.env.NEXT_PUBLIC_APP_IS_PROD ? "hexa-lite" : "",
- fee: 0.005,
+ fee: 0.05,
variant: "expandable",
insurance: true,
containerStyle: {
diff --git a/src/servcies/qrcode.service.ts b/src/servcies/qrcode.service.ts
new file mode 100644
index 00000000..0dd931f3
--- /dev/null
+++ b/src/servcies/qrcode.service.ts
@@ -0,0 +1,12 @@
+
+
+export const getQrcodeAsSVG = async (value: string) => {
+ const QRCode = await import("qrcode").then((module) => module);
+ try {
+ return await QRCode.toString(value, {
+ type: 'svg'
+ })
+ } catch (err: any) {
+ throw new Error(err?.message || "Error generating QR code")
+ }
+}
\ No newline at end of file
diff --git a/src/styles/global.scss b/src/styles/global.scss
index f54a9051..08253004 100755
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -693,4 +693,22 @@ div.MuiScopedCssBaseline-root {
}
.opacity-100 {
opacity: 1!important;
+}
+
+
+
+.plt-mobileweb, .plt-mobile {
+ ion-grid:has(.marketFilters) {
+ position: sticky;
+ top: -20px;
+ background: var(--ion-background-color);
+ z-index: 1;
+ }
+ div[id^=widget-relative-container] {
+ box-shadow: none;
+ }
+}
+
+.mobileConentModal {
+ --background: var(--ion-background-color);
}
\ No newline at end of file
From c37e30ea721cfd43425abc0252114b071cc14be5 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 11 Mar 2024 11:27:27 +0100
Subject: [PATCH 05/74] refactor: prevent overlap error with modal hook
---
src/components/PoolItem.tsx | 7 ++----
src/components/ReserveDetail.tsx | 14 ++++++------
src/components/mobile/WalletComponent.tsx | 17 ++++++++++++---
src/servcies/ankr.service.ts | 26 +++++++++++++++++++++++
4 files changed, 49 insertions(+), 15 deletions(-)
diff --git a/src/components/PoolItem.tsx b/src/components/PoolItem.tsx
index 7bac987b..c47b370f 100644
--- a/src/components/PoolItem.tsx
+++ b/src/components/PoolItem.tsx
@@ -1,6 +1,4 @@
import {
- IonAvatar,
- IonBadge,
IonButton,
IonCol,
IonFabButton,
@@ -85,7 +83,6 @@ export function PoolItem(props: IPoolItemProps) {
const { poolId, iconSize, chainId, handleSegmentChange } = props;
const { walletAddress, loadAssets } = Store.useState(getWeb3State);
const poolGroups = Store.useState(getPoolGroupsState);
- const modal = useRef(null);
const [isModalOpen, setIsModalOpen] = useState(false);
// find pool in `poolGroups[*].pool` by `poolId`
const pool = useMemo(() => {
@@ -232,7 +229,6 @@ export function PoolItem(props: IPoolItemProps) {
setIsModalOpen(() => false)}
@@ -240,7 +236,8 @@ export function PoolItem(props: IPoolItemProps) {
{
- modal.current?.dismiss();
+ setIsModalOpen(() => false);
+ // modal.current?.dismiss();
// reload asset if user have trigger an action from ReserveDetails.
// Ex: deposit, withdraw, borrow, repay
if (actionType) {
diff --git a/src/components/ReserveDetail.tsx b/src/components/ReserveDetail.tsx
index fcee47d9..7989ae6f 100644
--- a/src/components/ReserveDetail.tsx
+++ b/src/components/ReserveDetail.tsx
@@ -128,6 +128,7 @@ const loadTokenData = async (symbol: string) => {
export function ReserveDetail(props: IReserveDetailProps) {
const {
pool: { id, chainId },
+ dismiss,
handleSegmentChange,
} = props;
const {
@@ -146,7 +147,7 @@ export function ReserveDetail(props: IReserveDetailProps) {
>(undefined);
const poolGroups = Store.useState(getPoolGroupsState);
const userSummaryAndIncentivesGroup = Store.useState(getUserSummaryAndIncentivesGroupState);
- const [present, dismiss] = useIonToast();
+ const [present, dismissToast] = useIonToast();
const [presentAlert] = useIonAlert();
const [presentSuccess, dismissSuccess] = useIonModal(
() => (
@@ -217,7 +218,7 @@ export function ReserveDetail(props: IReserveDetailProps) {
>
);
const { display: displayLoader, hide: hideLoader } = useLoader();
- const modal = useRef(null);
+ // const modal = useRef(null);
const [isCrossChain, setIsCrossChain] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalOptionsOpen, setIsModalOptionsOpen] = useState(false);
@@ -491,12 +492,12 @@ export function ReserveDetail(props: IReserveDetailProps) {
return (
<>
-
-
+
+
props.dismiss(state?.actionType)}
+ onClick={() => dismiss(state?.actionType)}
>
@@ -1021,7 +1022,6 @@ export function ReserveDetail(props: IReserveDetailProps) {
{
setIsModalOpen(false);
@@ -1077,7 +1077,7 @@ export function ReserveDetail(props: IReserveDetailProps) {
text: "x",
role: "cancel",
handler: () => {
- dismiss();
+ dismissToast();
},
},
],
diff --git a/src/components/mobile/WalletComponent.tsx b/src/components/mobile/WalletComponent.tsx
index 232a87eb..f265452c 100644
--- a/src/components/mobile/WalletComponent.tsx
+++ b/src/components/mobile/WalletComponent.tsx
@@ -20,6 +20,7 @@ import {
IonItemSliding,
IonLabel,
IonList,
+ IonModal,
IonPage,
IonRow,
IonSearchbar,
@@ -32,7 +33,7 @@ import {
useIonRouter,
} from "@ionic/react";
import { paperPlane, download, repeat, card } from "ionicons/icons";
-import { ComponentRef, useMemo, useState } from "react";
+import { useMemo, useState } from "react";
import styleRule from "./WalletComponent.module.css";
import { IAsset } from "@/interfaces/asset.interface";
import { MobileDepositModal } from "./MobileDepositModal";
@@ -81,7 +82,7 @@ export default function WalletComponent() {
...selectedTokenDetail,
}
);
- const [presentEarn, dismissEarn] = useIonModal(MobileEarnModal, {});
+ const [isEarnModalOpen, setIsEarnModalOpen] = useState(false);
const modalOpts: Omit &
HookOverlayOptions = {
@@ -208,11 +209,21 @@ export default function WalletComponent() {
presentEarn(modalOpts)}
+ onClick={() => {
+ setIsEarnModalOpen(() => true);
+ }}
>
+ setIsEarnModalOpen(() => false)}
+ >
+
+
{assetGroup.length > 0 && (
diff --git a/src/servcies/ankr.service.ts b/src/servcies/ankr.service.ts
index d7e992e0..c976d636 100644
--- a/src/servcies/ankr.service.ts
+++ b/src/servcies/ankr.service.ts
@@ -37,6 +37,26 @@ const formatingTokensBalances = (assets: IAnkrTokenReponse[], address: string, c
});
}
+const getCachedData = async (key: string) => {
+ const data = localStorage.getItem(key);
+ if (!data) {
+ return null;
+ }
+ // check expiration cache using timestamp 10 minutes
+ const parsedData = JSON.parse(data);
+ if (Date.now() - parsedData.timestamp > 600000) {
+ return null;
+ }
+ console.log('[INFO] {ankrFactory} data from cache: ', parsedData.data);
+ return parsedData.data;
+}
+
+const setCachedData = async (key: string, data: any) => {
+ localStorage
+ .setItem(key, JSON.stringify({ data, timestamp: Date.now() }));
+
+}
+
/**
* Doc url: https://www.ankr.com/docs/app-chains/components/advanced-api/token-methods/#ankr_getaccountbalance
* @param chainIds array of chain ids
@@ -44,6 +64,11 @@ const formatingTokensBalances = (assets: IAnkrTokenReponse[], address: string, c
* @returns object with balances property that contains an array of TokenInterface
*/
export const getTokensBalances = async (chainIds: number[], address: string) => {
+ const KEY = `hexa-ankr-service-${address}`;
+ const cachedData = await getCachedData(KEY);
+ if (cachedData) {
+ return cachedData;
+ }
const APP_ANKR_APIKEY = process.env.NEXT_PUBLIC_APP_ANKR_APIKEY;
const chainsList =
chainIds.length <= 0
@@ -76,5 +101,6 @@ export const getTokensBalances = async (chainIds: number[], address: string) =>
const assets = (await res.json())?.result?.assets;
const balances = formatingTokensBalances(assets, address, chainsList);
console.log('[INFO] {ankrFactory} getTokensBalances(): ', balances);
+ await setCachedData(KEY, balances);
return balances;
};
\ No newline at end of file
From fbf94250c17128ac6d76df22c310520fbbf5a4cb Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Tue, 12 Mar 2024 17:46:45 +0100
Subject: [PATCH 06/74] refactor: add more ui improvments
---
package-lock.json | 67 +++
package.json | 3 +
src/components/AppShell.tsx | 153 +++++-
src/components/ETHLiquidStakingstrategy.tsx | 439 ++++++++++--------
src/components/MATICLiquidStakingstrategy.tsx | 51 +-
src/components/MarketsList.tsx | 9 +-
src/components/ReserveDetail.tsx | 216 +++++----
src/components/SymbolIcon.tsx | 2 +-
src/components/mobile/ActionNavButtons.tsx | 120 +++++
src/components/mobile/MobileEarnModal.tsx | 50 +-
.../mobile/MobileTokenDetailModal.tsx | 90 +++-
src/components/mobile/MobileTransferModal.tsx | 394 +++++++++++++++-
src/components/mobile/WalletComponent.tsx | 81 +---
src/styles/global.scss | 46 +-
14 files changed, 1312 insertions(+), 409 deletions(-)
create mode 100644 src/components/mobile/ActionNavButtons.tsx
diff --git a/package-lock.json b/package-lock.json
index 39a53d9a..d2bff43c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,8 +13,10 @@
"@aave/math-utils": "^1.21.1",
"@avalabs/avalanchejs": "^3.17.0",
"@bgd-labs/aave-address-book": "^2.19.0",
+ "@capacitor-community/barcode-scanner": "^4.0.1",
"@capacitor/android": "^5.0.0",
"@capacitor/core": "^5.0.0",
+ "@capacitor/dialog": "^5.0.7",
"@capacitor/ios": "^5.0.0",
"@capacitor/status-bar": "^5.0.0",
"@cosmjs/stargate": "^0.32.1",
@@ -41,6 +43,7 @@
"ethereum-blockies-base64": "^1.0.2",
"ethers": "^5.7.2",
"firebase": "^10.7.1",
+ "html5-qrcode": "^2.3.8",
"ionicons": "latest",
"lru-cache": "^10.1.0",
"magic-sdk": "^21.4.0",
@@ -1441,6 +1444,18 @@
"node": ">= 12"
}
},
+ "node_modules/@capacitor-community/barcode-scanner": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@capacitor-community/barcode-scanner/-/barcode-scanner-4.0.1.tgz",
+ "integrity": "sha512-acuhDU2mqskSeCIQMc5TGNnDszXXs4IqEES+3C2JDiq+MkJMTr+B2Dhq4k55hlkRFMOumMhlnbr2R9G6qyFPhw==",
+ "dependencies": {
+ "@zxing/browser": "^0.1.3",
+ "@zxing/library": "^0.20.0"
+ },
+ "peerDependencies": {
+ "@capacitor/core": "^5.0.0"
+ }
+ },
"node_modules/@capacitor/android": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.6.0.tgz",
@@ -1489,6 +1504,14 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/@capacitor/dialog": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/@capacitor/dialog/-/dialog-5.0.7.tgz",
+ "integrity": "sha512-lWNBHXOtt7V+Jk4YiShvnb+/4Ouo+yF1NKTOFpQXfVbsjrmmlXhd3ZSXSgMukEtyr0wr0phFUKDyamY08cYBOg==",
+ "peerDependencies": {
+ "@capacitor/core": "^5.0.0"
+ }
+ },
"node_modules/@capacitor/ios": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-5.6.0.tgz",
@@ -10734,6 +10757,37 @@
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="
},
+ "node_modules/@zxing/browser": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.4.tgz",
+ "integrity": "sha512-WYjaav7St4sj/u/Km2llE4NU2Pq3JFIWnczr0tmyCC1KUlp08rV3qpu7iiEB4kOx/CgcCzrSebNnSmFt5B3IFg==",
+ "optionalDependencies": {
+ "@zxing/text-encoding": "^0.9.0"
+ },
+ "peerDependencies": {
+ "@zxing/library": "^0.20.0"
+ }
+ },
+ "node_modules/@zxing/library": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz",
+ "integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==",
+ "dependencies": {
+ "ts-custom-error": "^3.2.1"
+ },
+ "engines": {
+ "node": ">= 10.4.0"
+ },
+ "optionalDependencies": {
+ "@zxing/text-encoding": "~0.9.0"
+ }
+ },
+ "node_modules/@zxing/text-encoding": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
+ "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
+ "optional": true
+ },
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -17800,6 +17854,11 @@
"void-elements": "3.1.0"
}
},
+ "node_modules/html5-qrcode": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
+ "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ=="
+ },
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -30081,6 +30140,14 @@
"typescript": ">=4.2.0"
}
},
+ "node_modules/ts-custom-error": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
+ "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
diff --git a/package.json b/package.json
index c075bd2a..df9d5907 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,10 @@
"@aave/math-utils": "^1.21.1",
"@avalabs/avalanchejs": "^3.17.0",
"@bgd-labs/aave-address-book": "^2.19.0",
+ "@capacitor-community/barcode-scanner": "^4.0.1",
"@capacitor/android": "^5.0.0",
"@capacitor/core": "^5.0.0",
+ "@capacitor/dialog": "^5.0.7",
"@capacitor/ios": "^5.0.0",
"@capacitor/status-bar": "^5.0.0",
"@cosmjs/stargate": "^0.32.1",
@@ -47,6 +49,7 @@
"ethereum-blockies-base64": "^1.0.2",
"ethers": "^5.7.2",
"firebase": "^10.7.1",
+ "html5-qrcode": "^2.3.8",
"ionicons": "latest",
"lru-cache": "^10.1.0",
"magic-sdk": "^21.4.0",
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index 66bfdab5..d304177e 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -14,6 +14,13 @@ import {
IonIcon,
useIonAlert,
IonImg,
+ IonSkeletonText,
+ IonFab,
+ IonFabButton,
+ IonList,
+ IonItem,
+ IonAvatar,
+ IonProgressBar,
} from "@ionic/react";
import { StatusBar, Style } from "@capacitor/status-bar";
@@ -56,10 +63,34 @@ window
const SwapContainer = lazy(() => import("@/containers/SwapContainer"));
const DefiContainer = lazy(() => import("@/containers/DefiContainer"));
const EarnContainer = lazy(() => import("@/containers/EarnContainer"));
-const MobileWalletComponent = lazy(() => import("@/components/mobile/WalletComponent"));
-const MobileWelcomeComponent = lazy(() => import("@/components/mobile/WelcomeComponent"));
+const MobileWalletComponent = lazy(
+ () => import("@/components/mobile/WalletComponent")
+);
+const MobileWelcomeComponent = lazy(
+ () => import("@/components/mobile/WelcomeComponent")
+);
-const isMobilePWADevice = true //Boolean(isPlatform('pwa') && !isPlatform('desktop'));
+const DefaultProgressBar = () => {
+ return ( )
+};
+const DefaultLoadingPage = () => {
+ return (
+
+
+
+
+
+ )
+}
+
+const isMobilePWADevice =
+ Boolean(isPlatform("pwa") && !isPlatform("desktop")) ||
+ Boolean(isPlatform("mobileweb")) ||
+ Boolean(isPlatform("mobile"));
const AppShell = () => {
// get params from url `s=`
@@ -159,20 +190,20 @@ const AppShell = () => {
/>
)}
- Loading SwapContainer...}>
+ }>
{currentSegment === "swap" && }
- Loading EarnContainer...}>
+ }>
{currentSegment === "earn" && }
- Loading DefiContainer...}>
+ }>
{currentSegment === "defi" && (
)}
- Loading NotFoundPage...}>
+ }>
{currentSegment === isNotFound && }
@@ -190,25 +221,109 @@ const AppShell = () => {
!walletAddress ?
- (
- Loading MobileWelcomeComponent...}>
+ render={() =>
+ !walletAddress ? (
+ }
+ >
- ) :
- (
- Loading MobileWalletComponent...}>
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[1,2,3,4,5].map((_: any, i: number) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ >}
+ >
)
}
/>
- (
-
- )}
- exact={true}
- />
+ } exact={true} />
)}
diff --git a/src/components/ETHLiquidStakingstrategy.tsx b/src/components/ETHLiquidStakingstrategy.tsx
index 6d58718d..febd9a38 100644
--- a/src/components/ETHLiquidStakingstrategy.tsx
+++ b/src/components/ETHLiquidStakingstrategy.tsx
@@ -1,4 +1,5 @@
import {
+ IonAvatar,
IonButton,
IonCard,
IonCardContent,
@@ -43,7 +44,7 @@ export interface IStrategyModalProps {
) => Promise | undefined;
}
-export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean }) {
+export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean, asItem?: boolean }) {
const {
currentNetwork,
web3Provider,
@@ -217,211 +218,248 @@ export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean }) {
return (
<>
-
-
-
-
-
-
-
-
-
-
- {strategy.name}
-
-
- {strategy.type}
-
-
-
-
+ {!props?.asItem && (
+
+
+
+
+
+
+
+
+
+
+ {strategy.name}
+
+
+ {strategy.type}
+
+
+
+
-
-
-
-
- Assets
-
-
- {strategy.assets.map((symbol, index) => (
-
- ))}
-
-
-
- Network
-
- {strategy.chainsId
- .map((id) => CHAIN_AVAILABLES.find((c) => c.id === id))
- .map((c, index) => {
- if (!c || !c.nativeSymbol) return null;
- return (
- 1
- ? "translateX(5px)"
- : "none",
- }}
- src={getAssetIconUrl({ symbol: c.nativeSymbol })}
- alt={c.nativeSymbol}
- />
- );
- })}
-
-
-
-
- APY
-
- <>
-
-
-
- Base APY (stETH)
-
-
- {strategy.apys[0]}%
-
-
-
-
- Total variable APY
-
-
-
- {strategy.apys[0]}%
-
-
- >
-
-
-
- {strategy.apys.map((apy, index) => (
-
- {apy}%
+
+
+
+
+ Assets
+
+
+ {strategy.assets.map((symbol, index) => (
+
+ ))}
+
+
+
+ Network
+
+ {strategy.chainsId
+ .map((id) => CHAIN_AVAILABLES.find((c) => c.id === id))
+ .map((c, index) => {
+ if (!c || !c.nativeSymbol) return null;
+ return (
+ 1
+ ? "translateX(5px)"
+ : "none",
+ }}
+ src={getAssetIconUrl({ symbol: c.nativeSymbol })}
+ alt={c.nativeSymbol}
+ />
+ );
+ })}
+
+
+
+
+ APY
+
+ <>
+
+
+
+ Base APY (stETH)
+
+
+ {strategy.apys[0]}%
+
+
+
+
+ Total variable APY
+
+
+
+ {strategy.apys[0]}%
+
+
+ >
+
+
+
+ {strategy.apys.map((apy, index) => (
+
+ {apy}%
+
+ ))}
+
+
+
+ Protocols
+
+ {strategy.providers
+ .map((p) => {
+ // return capitalized string
+ return p.charAt(0).toUpperCase() + p.slice(1);
+ })
+ .join(" + ")}
+
+
+
+
+
+
+
+
+
+
+ Staking ETH with Lido
+
+
+ By swapping your ETH for wstETH, you will increase your ETH holdings
+ by {baseAPRstETH.toFixed(2)}% APY using ETH staking with{" "}
+
+ Lido finance
+ .
+
+
+
+
+ The wstETH price increases daily with exchange rate reflecting staking rewards.
+
+
+
+
+ You can also use your wstETH to earn more yield on lendings market or swap back to ETH at any time without locking period.
+
+
- ))}
-
-
-
- Protocols
-
- {strategy.providers
- .map((p) => {
- // return capitalized string
- return p.charAt(0).toUpperCase() + p.slice(1);
- })
- .join(" + ")}
-
-
-
-
+
+
-
-
-
-
-
- Staking ETH with Lido
-
-
- By swapping your ETH for wstETH, you will increase your ETH holdings
- by {baseAPRstETH.toFixed(2)}% APY using ETH staking with{" "}
-
- Lido finance
- .
-
-
-
-
- The wstETH price increases daily with exchange rate reflecting staking rewards.
-
-
-
-
- You can also use your wstETH to earn more yield on lendings market or swap back to ETH at any time without locking period.
-
-
-
-
-
+ {
+ const chainId = currentNetwork;
+ await displayLoader();
+ if (chainId !== NETWORK.optimism) {
+ await switchNetwork(NETWORK.optimism);
+ }
+ await modal.current?.present();
+ await hideLoader();
+ }}
+ expand="block"
+ color="gradient"
+ >
+ Start Earning
+
+
+
- {
- const chainId = currentNetwork;
- await displayLoader();
- if (chainId !== NETWORK.optimism) {
- await switchNetwork(NETWORK.optimism);
- }
- await modal.current?.present();
- await hideLoader();
- }}
- expand="block"
- color="gradient"
- >
- Start Earning
-
-
-
+
+
+
+ ) }
-
-
-
+
+ {props?.asItem && !props?.asImage && (
+ {
+ const chainId = currentNetwork;
+ await displayLoader();
+ if (chainId !== NETWORK.optimism) {
+ await switchNetwork(NETWORK.optimism);
+ }
+ await modal.current?.present();
+ await hideLoader();
+ }}>
+
+
+
+
+
+ {strategy.name}
+
+
+
+ {strategy.type}
+
+
+
+
+
+ {strategy.apys[0]}%
+
+
+
+ )}
) => {
- console.log("will dismiss", ev.detail);
- }}
className="modalPage"
>
@@ -458,7 +496,7 @@ export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean }) {
fontSize: '2.4rem',
lineHeight: '1.85rem'
}}>
-
+
{strategy.name}
@@ -466,7 +504,8 @@ export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean }) {
marginBottom: '1.5rem',
fontSize: '1.4rem',
lineHeight: '1.15rem'
- }}>{strategy.type}
+ }}
+ className="ion-color-gradient-text">{strategy.type}
diff --git a/src/components/MATICLiquidStakingstrategy.tsx b/src/components/MATICLiquidStakingstrategy.tsx
index 0a779692..eef85301 100644
--- a/src/components/MATICLiquidStakingstrategy.tsx
+++ b/src/components/MATICLiquidStakingstrategy.tsx
@@ -1,4 +1,5 @@
import {
+ IonAvatar,
IonButton,
IonCard,
IonCardContent,
@@ -46,7 +47,7 @@ export interface IStrategyModalProps {
) => Promise | undefined;
}
-export function MATICLiquidStakingstrategyCard() {
+export function MATICLiquidStakingstrategyCard(props: { asImage?: boolean, asItem?: boolean }) {
const { web3Provider, switchNetwork, connectWallet, disconnectWallet, currentNetwork } = Store.useState(getWeb3State);
const [baseAPRst, setBaseAPRst] = useState(-1);
const [action, setAction] = useState<"stake" | "unstake">("stake");
@@ -203,6 +204,7 @@ export function MATICLiquidStakingstrategyCard() {
return (
<>
+ {!props?.asItem && (
@@ -220,10 +222,10 @@ export function MATICLiquidStakingstrategyCard() {
-
+
{strategy.name}
-
+
{strategy.type}
@@ -404,6 +406,44 @@ export function MATICLiquidStakingstrategyCard() {
+ )}
+
+ {props?.asItem && !props?.asImage && (
+ {
+ const chainId = currentNetwork;
+ await displayLoader();
+ if (chainId !== NETWORK.optimism) {
+ await switchNetwork(NETWORK.optimism);
+ }
+ await modal.current?.present();
+ await hideLoader();
+ }}>
+
+
+
+
+
+ {strategy.name}
+
+
+
+ {strategy.type}
+
+
+
+
+
+ {strategy.apys[0]}%
+
+
+
+ )}
-
+
{strategy.name}
{strategy.type}
+ }}
+ className="ion-color-gradient-text">{strategy.type}
diff --git a/src/components/MarketsList.tsx b/src/components/MarketsList.tsx
index 0e582adb..e0fd8e6b 100644
--- a/src/components/MarketsList.tsx
+++ b/src/components/MarketsList.tsx
@@ -97,7 +97,14 @@ export function MarketList(props: {
return (
<>
-
+
{
- // check if have localstorage data
+ // check if have localstorage data
const localCoinsListString = localStorage.getItem("coingecko-coins-list");
- let localCoinsList = localCoinsListString ? JSON.parse(localCoinsListString) : null;
+ let localCoinsList = localCoinsListString
+ ? JSON.parse(localCoinsListString)
+ : null;
if (!localCoinsList) {
- localCoinsList = await fetch(`https://api.coingecko.com/api/v3/coins/list`)
- .then((response) => response.json());
- localStorage.setItem("coingecko-coins-list", JSON.stringify(localCoinsList));
+ localCoinsList = await fetch(
+ `https://api.coingecko.com/api/v3/coins/list`
+ ).then((response) => response.json());
+ localStorage.setItem(
+ "coingecko-coins-list",
+ JSON.stringify(localCoinsList)
+ );
}
if (!localCoinsList) {
return;
}
// find coin id by symbol
- const coin = localCoinsList.find((coin: {symbol: string}) => coin.symbol.toLocaleLowerCase() === symbol.toLocaleLowerCase());
+ const coin = localCoinsList.find(
+ (coin: { symbol: string }) =>
+ coin.symbol.toLocaleLowerCase() === symbol.toLocaleLowerCase()
+ );
if (coin) {
// fetch coin data by id
return fetch(`https://api.coingecko.com/api/v3/coins/${coin.id}`)
.then((response) => response.json())
.then((data) => {
console.log("coin data: ", data.description.en);
- const {
- description: {en: description},
+ const {
+ description: { en: description },
market_data: {
- fully_diluted_valuation: {usd: fullyDilutedValuationUSD},
- market_cap: {usd: marketCapUSD},
+ fully_diluted_valuation: { usd: fullyDilutedValuationUSD },
+ market_cap: { usd: marketCapUSD },
max_supply: maxSupply,
total_supply: totalSupply,
- circulating_supply: circulatingSupply
- }
+ circulating_supply: circulatingSupply,
+ },
} = data;
return {
description,
@@ -117,13 +127,13 @@ const loadTokenData = async (symbol: string) => {
marketCapUSD,
maxSupply,
totalSupply,
- circulatingSupply
- }
+ circulatingSupply,
+ };
});
} else {
- return
+ return;
}
-}
+};
export function ReserveDetail(props: IReserveDetailProps) {
const {
@@ -146,29 +156,29 @@ export function ReserveDetail(props: IReserveDetailProps) {
| undefined
>(undefined);
const poolGroups = Store.useState(getPoolGroupsState);
- const userSummaryAndIncentivesGroup = Store.useState(getUserSummaryAndIncentivesGroupState);
+ const userSummaryAndIncentivesGroup = Store.useState(
+ getUserSummaryAndIncentivesGroupState
+ );
const [present, dismissToast] = useIonToast();
const [presentAlert] = useIonAlert();
- const [presentSuccess, dismissSuccess] = useIonModal(
- () => (
-
-
-
-
- {state?.actionType.toLocaleUpperCase()} with Success!
-
-
-
- )
- );
+ const [presentSuccess, dismissSuccess] = useIonModal(() => (
+
+
+
+
+ {state?.actionType.toLocaleUpperCase()} with Success!
+
+
+
+ ));
const [presentPomptCrossModal, dismissPromptCrossModal] = useIonModal(
<>
@@ -489,29 +499,26 @@ export function ReserveDetail(props: IReserveDetailProps) {
// setTokenDetails(() => details);
// });
// }, [pool.symbol]);
-
+
return (
<>
+
+
+
+ Market details
+
+
+ dismiss(state?.actionType)}
+ >
+
+
+
+
+
-
- dismiss(state?.actionType)}
- >
-
-
-
-
-
Market details
-
-
-
-
- {pool?.symbol}
-
-
- {
- CHAIN_AVAILABLES.find(
- (c) => c.id === pool.chainId
- )?.name
- }{" "}
- network
-
-
-
- {pool.usageAsCollateralEnabled === false && (
-
- )}
-
+
+
+
+
+
+
+
+ {pool?.symbol}
+
+
+ {
+ CHAIN_AVAILABLES.find(
+ (c) => c.id === pool.chainId
+ )?.name
+ }{" "}
+ network
+
+
+
+ {pool.usageAsCollateralEnabled === false && (
+
+ )}
+
+
+
-
+
{walletAddress ? (
<>
{tokenDetails && (
-
+
Token details
diff --git a/src/components/SymbolIcon.tsx b/src/components/SymbolIcon.tsx
index 9feaf415..835bc67b 100644
--- a/src/components/SymbolIcon.tsx
+++ b/src/components/SymbolIcon.tsx
@@ -18,7 +18,7 @@ export function SymbolIcon(props: {symbol: string; chainId?: number; iconSize?:
) : null;
return (
{
+ const { selectedTokenDetail, hideEarnBtn = false } = props;
+
+ const [presentDeposit, dismissDeposit] = useIonModal(MobileDepositModal, {
+ ...selectedTokenDetail,
+ });
+ const [presentSwap, dismissSwap] = useIonModal(MobileSwapModal, {
+ ...selectedTokenDetail,
+ });
+ const [presentTokenDetail, dismissTokenDetail] = useIonModal(
+ MobileTokenDetailModal,
+ { ...selectedTokenDetail }
+ );
+ const [isEarnModalOpen, setIsEarnModalOpen] = useState(false);
+ const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
+
+ const modalOpts: Omit &
+ HookOverlayOptions = {
+ initialBreakpoint: 0.98,
+ breakpoints: [0, 0.98],
+ };
+
+ return (
+
+
+
+ setIsTransferModalOpen(true)}
+ >
+
+
+
+ setIsTransferModalOpen(() => false)}
+ >
+
+
+
+
+
+ presentDeposit(modalOpts)}
+ >
+
+
+
+
+
+
+ presentSwap(modalOpts)}
+ >
+
+
+
+
+ { hideEarnBtn !== true && (
+
+
+ {
+ setIsEarnModalOpen(() => true);
+ }}
+ >
+
+
+
+ setIsEarnModalOpen(() => false)}
+ >
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/mobile/MobileEarnModal.tsx b/src/components/mobile/MobileEarnModal.tsx
index ca18da55..094df6b8 100644
--- a/src/components/mobile/MobileEarnModal.tsx
+++ b/src/components/mobile/MobileEarnModal.tsx
@@ -1,8 +1,12 @@
import {
+ IonAvatar,
IonCol,
IonContent,
IonGrid,
IonHeader,
+ IonItem,
+ IonLabel,
+ IonList,
IonRow,
IonSegment,
IonSegmentButton,
@@ -25,6 +29,8 @@ import {
import { patchPoolsState } from "@/store/actions";
import { CHAIN_AVAILABLES } from "@/constants/chains";
import { getReadableValue } from "@/utils/getReadableValue";
+import { ETHLiquidStakingstrategyCard } from "../ETHLiquidStakingstrategy";
+import { MATICLiquidStakingstrategyCard } from "../MATICLiquidStakingstrategy";
export const MobileEarnModal = () => {
const [segment, setSegment] = useState("loan");
@@ -87,7 +93,10 @@ export const MobileEarnModal = () => {
{segment === "loan" && (
-
+
Available Markets
@@ -162,6 +171,45 @@ export const MobileEarnModal = () => {
)}
+
+ {segment === "earn" && (
+
+
+
+
+ Earn interest
+
+
+
+
+ Unlock the full potential of your assets by earning
+ intrest through Liquid Staking or Providing Liquidity to
+ the markets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
>
);
diff --git a/src/components/mobile/MobileTokenDetailModal.tsx b/src/components/mobile/MobileTokenDetailModal.tsx
index 095f64ed..c7f57337 100644
--- a/src/components/mobile/MobileTokenDetailModal.tsx
+++ b/src/components/mobile/MobileTokenDetailModal.tsx
@@ -1,18 +1,38 @@
import { IAsset } from "@/interfaces/asset.interface";
import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
-import { IonAvatar, IonCol, IonContent, IonGrid, IonRow, IonText } from "@ionic/react"
+import { IonAvatar, IonCol, IonContent, IonGrid, IonItem, IonLabel, IonList, IonListHeader, IonNote, IonRow, IonText } from "@ionic/react"
+import { MobileActionNavButtons } from "./ActionNavButtons";
+import { useEffect } from "react";
+import { ethers } from "ethers";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import { CHAIN_AVAILABLES } from "@/constants/chains";
+const getTxsFromAddress = async (address: string) => {
+ let provider = new ethers.providers.EtherscanProvider();
+ let history = await provider.getHistory(address);
+ console.log(history);
+}
export const MobileTokenDetailModal = (props: {
- name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[];
+ data: {name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[]};
+ dismiss: () => void;
})=> {
+ const { data, dismiss } = props;
+ const { walletAddress } = Store.useState(getWeb3State);
+
+ useEffect(() => {
+ if (!walletAddress) return;
+ getTxsFromAddress(walletAddress);
+ }, [walletAddress]);
+
return (
-
-
+
+
{
(
event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${props.symbol}&bgColor=%23000000&textColor=%23182449`;
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${data.symbol}&bgColor=%23000000&textColor=%23182449`;
}}
/>
- {props.symbol}
+
+ {data.balance.toFixed(6)} {data.symbol}
+
- {props.name}
+
+ $ {data.balanceUsd.toFixed(2)}
+
+
+
+
-
- $ {props.balanceUsd.toFixed(2)}
- {props.balance.toFixed(6)}
-
+
+
+
+ Networks
+
+
+ {data.assets.map((token, index) =>
+
+
+ c.id === token.chain?.id)?.logo}
+ alt={token.symbol}
+ style={{ transform: "scale(1.01)"}}
+ onError={(event) => {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {token.chain?.name}
+
+
+
+ {token.balance.toFixed(6)} {token.symbol}
+
+ $ {token.balanceUsd.toFixed(2)}
+
+
+
+
+ )}
+
diff --git a/src/components/mobile/MobileTransferModal.tsx b/src/components/mobile/MobileTransferModal.tsx
index 8b171ab3..8cf5dbc2 100644
--- a/src/components/mobile/MobileTransferModal.tsx
+++ b/src/components/mobile/MobileTransferModal.tsx
@@ -1,9 +1,336 @@
import { IAsset } from "@/interfaces/asset.interface";
-import { IonCol, IonContent, IonGrid, IonRow, IonText } from "@ionic/react";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import {
+ IonButton,
+ IonCol,
+ IonContent,
+ IonFab,
+ IonFabButton,
+ IonGrid,
+ IonIcon,
+ IonInput,
+ IonItem,
+ IonLabel,
+ IonList,
+ IonListHeader,
+ IonModal,
+ IonPopover,
+ IonRow,
+ IonText,
+} from "@ionic/react";
+import { chevronDown, close, scan } from "ionicons/icons";
+import { SymbolIcon } from "../SymbolIcon";
+import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from "react";
+import { CHAIN_AVAILABLES, CHAIN_DEFAULT } from "@/constants/chains";
+import { getReadableAmount } from "@/utils/getReadableAmount";
+import { InputInputEventDetail, IonInputCustomEvent } from "@ionic/core";
+import { Html5Qrcode } from "html5-qrcode";
+
+const isNumberKey = (evt: React.KeyboardEvent) => {
+ var charCode = evt.which ? evt.which : evt.keyCode;
+ return !(charCode > 31 && (charCode < 48 || charCode > 57));
+};
+
+const scanQrCode = async (html5QrcodeScanner: Html5Qrcode): Promise => {
+ try {
+ const qrboxFunction = function(viewfinderWidth: number, viewfinderHeight: number) {
+ // Square QR Box, with size = 80% of the min edge width.
+ const size = Math.min(viewfinderWidth, viewfinderHeight) * 0.8;
+ return {
+ width: size,
+ height: size
+ };
+ };
+ const cameras = await Html5Qrcode.getCameras();
+ if (!cameras || cameras.length === 0) {
+ throw new Error("No camera found");
+ }
+
+ // get prefered back camera if available or load the first one
+ const cameraId = cameras.find((c) => c.label.includes("back"))?.id || cameras[0].id;
+ console.log('>>', cameraId, cameras)
+ // start scanner
+ const config = {
+ fps: 10,
+ qrbox: qrboxFunction,
+ // Important notice: this is experimental feature, use it at your
+ // own risk. See documentation in
+ // mebjas@/html5-qrcode/src/experimental-features.ts
+ experimentalFeatures: {
+ useBarCodeDetectorIfSupported: true
+ },
+ rememberLastUsedCamera: true,
+ showTorchButtonIfSupported: true
+ };
+ if (!cameraId) {
+ throw new Error("No camera found");
+ }
+ // If you want to prefer front camera
+ return new Promise((resolve, reject) => {
+ html5QrcodeScanner.start(cameraId, config, (decodedText, decodedResult)=> {
+ // stop reader
+ html5QrcodeScanner.stop();
+ // resolve promise with the decoded text
+ resolve(decodedText);
+ }, (error) =>{});
+ });
+ } catch (error: any) {
+ throw new Error(error?.message || "BarcodeScanner not available");
+ }
+};
+
+const ScanModal = (props: { isOpen: boolean, onDismiss: (address?: string) => void }) => {
+ const [html5Qrcode, setHtml5Qrcode] = useState();
+ const elementRef = useRef(null);
+
+ useEffect(() => {
+ if (!props.isOpen) {
+ return;
+ }
+ console.log('>>>>', elementRef.current)
+ if (!elementRef.current) {
+ return;
+ }
+ if (!html5Qrcode) {
+ throw new Error("BarcodeScanner not available");
+ }
+ const scaner = new html5Qrcode('reader-scan-element')
+ if (!scaner) {
+ throw new Error("BarcodeScanner not loaded");
+ }
+ try {
+ scanQrCode(scaner).then(
+ result => {
+ scaner.stop();
+ props.onDismiss(result);
+ }
+ );
+ } catch (error: any) {
+ console.error(error);
+ scaner.stop();
+ }
+ }, [elementRef.current, html5Qrcode, props.isOpen]);
+
+ return (
+ {
+ import("html5-qrcode").then(
+ (m) => setHtml5Qrcode(()=> (m.Html5Qrcode))
+ );
+ }}
+ onDidDismiss={()=> props.onDismiss()}>
+
+
+ props.onDismiss()}>
+
+
+
+
+
+
+ )
+};
+
+const InputAssetWithDropDown = (props: {
+ assets: IAsset[];
+ inputFromAmount: number;
+ setInputFromAmount: Dispatch>;
+}) => {
+ const { assets, setInputFromAmount, inputFromAmount } = props;
+ const [errorMessage, setErrorMessage] = useState();
+ const [selectedAsset, setSelectedAsset] = useState(assets[0]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [popoverOpen, setPopoverOpen] = useState(false);
+ const popover = useRef(null);
+
+ const maxBalance = useMemo(() => {
+ // round to the lower tenth
+ return Math.floor(selectedAsset?.balance * 10000) / 10000;
+ }, [selectedAsset]);
+
+ const handleInputChange = async (
+ e: IonInputCustomEvent
+ ) => {
+ let value = Number((e.target as any).value || 0);
+ if (maxBalance && value > maxBalance) {
+ (e.target as any).value = maxBalance;
+ value = maxBalance;
+ }
+ if (value <= 0) {
+ setErrorMessage(() => undefined);
+ // UI loader control
+ setIsLoading(() => false);
+ return;
+ }
+ setInputFromAmount(() => value);
+ setErrorMessage(() => undefined);
+ // UI loader control
+ setIsLoading(() => false);
+ };
+
+ return (
+
+ {
+ $event.stopPropagation();
+ // set position
+ popover.current!.event = $event;
+ // open popover
+ setPopoverOpen(() => true);
+ }}
+ >
+
+
+
+
+
+
setPopoverOpen(false)}
+ >
+
+
+
+ Available assets
+
+
+
+ {assets
+ .filter((a) => a.balance > 0)
+ .map((asset, index) => (
+ {
+ setPopoverOpen(() => true);
+ setSelectedAsset(asset);
+ setInputFromAmount(() => 0);
+ setErrorMessage(() => undefined);
+ // setQuote(() => undefined);
+ console.log({ selectedAsset });
+ }}
+ >
+
+
+
+
+ {asset.symbol}
+
+
+
+ {
+ CHAIN_AVAILABLES.find(
+ (c) => c.id === asset?.chain?.id
+ )?.name
+ }
+
+
+
+
+ {Number(asset?.balance).toFixed(6)}
+
+
+
+ {getReadableAmount(
+ +asset?.balance,
+ Number(asset?.priceUsd),
+ "No deposit"
+ )}
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {selectedAsset?.symbol}
+
+ {
+ $event.stopPropagation();
+ setInputFromAmount(() => selectedAsset?.balance || 0);
+ }}
+ >
+ Max :{maxBalance}
+
+
+
+
+
+ {
+ if (isNumberKey(e)) {
+ setIsLoading(() => true);
+ }
+ }}
+ onIonInput={(e) => handleInputChange(e)}
+ />
+
+
+ );
+};
+
+export const MobileTransferModal = () => {
+ const { walletAddress, isMagicWallet, assets } = Store.useState(getWeb3State);
+ const [inputFromAmount, setInputFromAmount] = useState(0);
+ const [inputToAddress, setInputToAddress] = useState(undefined);
+ const [isScanModalOpen, setIsScanModalOpen] = useState(false);
+
+ const isValid = inputFromAmount > 0 && inputToAddress && inputToAddress.length > 0;
-export const MobileTransferModal = (props: {
- name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[];
- }) => {
return (
@@ -13,7 +340,64 @@ export const MobileTransferModal = (props: {
Send token
- xxx
+
+
+
+
+
+
+ Destination address
+
+ {
+ console.log($event)
+ setInputToAddress(() => ($event.detail.value|| undefined));
+ }} />
+ {
+ setIsScanModalOpen(()=> true);
+ }}
+ >
+
+
+
+ {
+ if (data) {
+ setInputToAddress(() => data);
+ }
+ setIsScanModalOpen(() => false);
+ }} />
+
+
+ Send
+
diff --git a/src/components/mobile/WalletComponent.tsx b/src/components/mobile/WalletComponent.tsx
index f265452c..2bd48cfd 100644
--- a/src/components/mobile/WalletComponent.tsx
+++ b/src/components/mobile/WalletComponent.tsx
@@ -41,18 +41,9 @@ import { HookOverlayOptions } from "@ionic/react/dist/types/hooks/HookOverlayOpt
import { MobileTransferModal } from "./MobileTransferModal";
import { getMagic } from "@/servcies/magic";
import { MobileSwapModal } from "./MobileSwapModal";
-import { MobileEarnModal } from "./MobileEarnModal";
import { MobileTokenDetailModal } from "./MobileTokenDetailModal";
import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
-
-const style = {
- fullHeight: {
- height: "100%",
- },
- fab: {
- display: "contents",
- },
-};
+import { MobileActionNavButtons } from "./ActionNavButtons";
export default function WalletComponent() {
const { walletAddress, isMagicWallet, assets } = Store.useState(getWeb3State);
@@ -67,22 +58,23 @@ export default function WalletComponent() {
thumbnail: string;
assets: IAsset[];
} | null>(null);
- const [presentTransfer, dismissTransfer] = useIonModal(MobileTransferModal, {
+
+ const [presentTransfer] = useIonModal(MobileTransferModal, {
...selectedTokenDetail,
});
- const [presentDeposit, dismissDeposit] = useIonModal(MobileDepositModal, {
+ const [presentDeposit] = useIonModal(MobileDepositModal, {
...selectedTokenDetail,
});
- const [presentSwap, dismissSwap] = useIonModal(MobileSwapModal, {
+ const [presentSwap] = useIonModal(MobileSwapModal, {
...selectedTokenDetail,
});
const [presentTokenDetail, dismissTokenDetail] = useIonModal(
MobileTokenDetailModal,
- {
- ...selectedTokenDetail,
- }
+ { data: selectedTokenDetail, dismiss: () => {
+ dismissTokenDetail();
+ setSelectedTokenDetail(null);
+ }}
);
- const [isEarnModalOpen, setIsEarnModalOpen] = useState(false);
const modalOpts: Omit &
HookOverlayOptions = {
@@ -174,58 +166,9 @@ export default function WalletComponent() {
-
-
-
- presentTransfer(modalOpts)}
- >
-
-
-
-
-
-
- presentDeposit(modalOpts)}
- >
-
-
-
-
-
-
- presentSwap(modalOpts)}
- >
-
-
-
-
-
-
- {
- setIsEarnModalOpen(() => true);
- }}
- >
-
-
-
- setIsEarnModalOpen(() => false)}
- >
-
-
-
-
+
+
+
{assetGroup.length > 0 && (
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 08253004..21c28291 100755
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -194,6 +194,11 @@ ion-alert.modalAlert {
--border-radius: 0;
}
+.modal-sheet {
+ --width: 100%;
+ --border-radius: 32px;
+}
+
.ion-accordion-toggle-icon {
color: var(--ion-color-primary);
@@ -707,8 +712,45 @@ div.MuiScopedCssBaseline-root {
div[id^=widget-relative-container] {
box-shadow: none;
}
+
+ .modalPage ion-content {
+ --background: var(--ion-background-color);
+
+ .widgetWrapper {
+ border: none;
+ box-shadow: none;
+ -webkit-backdrop-filter: none;
+ backdrop-filter: none;
+ background: transparent;
+
+ > ion-col {
+ text-align: center;
+ }
+ }
+
+ }
+
+ div[id^="widget-relative-container"] {
+ border: none;
+ }
+
+ div.MuiScopedCssBaseline-root {
+ background: transparent!important;
+ -webkit-backdrop-filter: none!important;
+ backdrop-filter: none!important;
+
+ >div[id^="widget-scrollable-container"] {
+ -webkit-backdrop-filter: none!important;
+ backdrop-filter: none!important;
+ }
+ }
}
-.mobileConentModal {
+.mobileConentModal{
--background: var(--ion-background-color);
-}
\ No newline at end of file
+}
+// .reader-scan-element {
+// video {
+// height: 100%;
+// }
+// }
\ No newline at end of file
From 1eeb437f086f298ce25fe12529214cb7692f6532 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Wed, 13 Mar 2024 11:20:26 +0100
Subject: [PATCH 07/74] refactor: Refactor components and Containers
---
src/components/AppShell.tsx | 32 +-
src/components/ETHLiquidStakingstrategy.tsx | 112 +++--
src/components/Header.tsx | 6 +-
src/components/LoanFormModal.tsx | 1 +
src/components/MATICLiquidStakingstrategy.tsx | 111 +++--
src/components/ReserveDetail.tsx | 1 +
src/components/base/WalletBaseContainer.tsx | 170 +++++++
src/components/mobile/ActionNavButtons.tsx | 144 +++---
.../mobile/WalletComponent.module.css | 12 -
src/components/mobile/WalletComponent.tsx | 388 ----------------
.../DepositContainer.tsx} | 4 +-
.../TransferContainer.tsx} | 155 ++++---
.../{ => desktop}/DefiContainer.tsx | 10 +-
.../{ => desktop}/EarnContainer.tsx | 4 +-
.../{ => desktop}/FiatContainer.tsx | 0
.../{ => desktop}/LeaderboardContainer.tsx | 0
.../{ => desktop}/SwapContainer.tsx | 8 +-
.../desktop/WalletDesktopContainer.tsx | 326 ++++++++++++++
.../mobile/EarnMobileContainer.tsx} | 8 +-
.../mobile/SwapMobileContainer.tsx} | 24 +-
.../mobile/TokenDetailMobileContainer.tsx} | 24 +-
.../mobile/WalletMobileContainer.tsx | 422 ++++++++++++++++++
.../mobile/WelcomeMobileContainer.tsx} | 11 +-
23 files changed, 1281 insertions(+), 692 deletions(-)
create mode 100644 src/components/base/WalletBaseContainer.tsx
delete mode 100644 src/components/mobile/WalletComponent.module.css
delete mode 100644 src/components/mobile/WalletComponent.tsx
rename src/{components/mobile/MobileDepositModal.tsx => containers/DepositContainer.tsx} (97%)
rename src/{components/mobile/MobileTransferModal.tsx => containers/TransferContainer.tsx} (78%)
rename src/containers/{ => desktop}/DefiContainer.tsx (98%)
rename src/containers/{ => desktop}/EarnContainer.tsx (95%)
rename src/containers/{ => desktop}/FiatContainer.tsx (100%)
rename src/containers/{ => desktop}/LeaderboardContainer.tsx (100%)
rename src/containers/{ => desktop}/SwapContainer.tsx (96%)
create mode 100644 src/containers/desktop/WalletDesktopContainer.tsx
rename src/{components/mobile/MobileEarnModal.tsx => containers/mobile/EarnMobileContainer.tsx} (96%)
rename src/{components/mobile/MobileSwapModal.tsx => containers/mobile/SwapMobileContainer.tsx} (92%)
rename src/{components/mobile/MobileTokenDetailModal.tsx => containers/mobile/TokenDetailMobileContainer.tsx} (83%)
create mode 100644 src/containers/mobile/WalletMobileContainer.tsx
rename src/{components/mobile/WelcomeComponent.tsx => containers/mobile/WelcomeMobileContainer.tsx} (74%)
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index d304177e..c40e2319 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -28,14 +28,10 @@ import { IonReactRouter } from "@ionic/react-router";
import { Redirect, Route, useHistory } from "react-router-dom";
import { useEffect, useRef, useState, lazy, Suspense } from "react";
import { Welcome } from "./Welcome";
-// import { SwapContainer } from '@/containers/SwapContainer';
-// import { FiatContainer } from "@/containers/FiatContainer";
-// import { DefiContainer } from '@/containers/DefiContainer';
-// import { EarnContainer } from '@/containers/EarnContainer';
import { Header } from "./Header";
import MenuSlide from "./MenuSlide";
import { LoaderProvider } from "@/context/LoaderContext";
-import { Leaderboard } from "@/containers/LeaderboardContainer";
+import { Leaderboard } from "@/containers/desktop/LeaderboardContainer";
import { NotFoundPage } from "@/containers/NotFoundPage";
import PwaInstall from "./PwaInstall";
import { initializeWeb3 } from "@/store/effects/web3.effects";
@@ -60,14 +56,15 @@ window
} catch {}
});
-const SwapContainer = lazy(() => import("@/containers/SwapContainer"));
-const DefiContainer = lazy(() => import("@/containers/DefiContainer"));
-const EarnContainer = lazy(() => import("@/containers/EarnContainer"));
-const MobileWalletComponent = lazy(
- () => import("@/components/mobile/WalletComponent")
+const WalletDesktopContainer = lazy(() => import("@/containers/desktop/WalletDesktopContainer"));
+const SwapContainer = lazy(() => import("@/containers/desktop/SwapContainer"));
+const DefiContainer = lazy(() => import("@/containers/desktop/DefiContainer"));
+const EarnContainer = lazy(() => import("@/containers/desktop/EarnContainer"));
+const WalletMobileContainer = lazy(
+ () => import("@/containers/mobile/WalletMobileContainer")
);
-const MobileWelcomeComponent = lazy(
- () => import("@/components/mobile/WelcomeComponent")
+const WelcomeMobileContainer = lazy(
+ () => import("@/containers/mobile/WelcomeMobileContainer")
);
const DefaultProgressBar = () => {
@@ -100,7 +97,7 @@ const AppShell = () => {
const [presentFiatWarning, dismissFiatWarning] = useIonAlert();
const isNotFound =
- segment && ["swap", "fiat", "defi", "earn"].indexOf(segment) === -1;
+ segment && ["wallet", "swap", "fiat", "defi", "earn"].indexOf(segment) === -1;
// use state to handle segment change
const [currentSegment, setSegment] = useState(segment);
const handleSegmentChange = async (e: any) => {
@@ -191,7 +188,10 @@ const AppShell = () => {
)}
}>
- {currentSegment === "swap" && }
+ {currentSegment === "wallet" && ( )}
+
+ }>
+ {currentSegment === "swap" && ( )}
}>
{currentSegment === "earn" && }
@@ -226,7 +226,7 @@ const AppShell = () => {
}
>
-
+
) : (
{
>}
>
-
+
)
}
diff --git a/src/components/ETHLiquidStakingstrategy.tsx b/src/components/ETHLiquidStakingstrategy.tsx
index febd9a38..d93512ca 100644
--- a/src/components/ETHLiquidStakingstrategy.tsx
+++ b/src/components/ETHLiquidStakingstrategy.tsx
@@ -1,11 +1,13 @@
import {
IonAvatar,
IonButton,
+ IonButtons,
IonCard,
IonCardContent,
IonCol,
IonContent,
IonGrid,
+ IonHeader,
IonIcon,
IonImg,
IonItem,
@@ -15,6 +17,8 @@ import {
IonSegment,
IonSegmentButton,
IonText,
+ IonTitle,
+ IonToolbar,
useIonToast,
} from "@ionic/react";
import { ethers } from "ethers";
@@ -176,7 +180,8 @@ export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean, asItem?
},
hiddenUI: [
...LIFI_CONFIG?.hiddenUI as any,
- HiddenUI.ToAddress
+ HiddenUI.ToAddress,
+ HiddenUI.History,
],
disabledUI: action === 'stake'
? [ "toToken"]
@@ -435,8 +440,9 @@ export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean, asItem?
}}>
@@ -444,8 +450,8 @@ export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean, asItem?
{strategy.name}
-
-
+
+
{strategy.type}
@@ -462,51 +468,75 @@ export function ETHLiquidStakingstrategyCard(props: { asImage?: boolean, asItem?
ref={modal}
className="modalPage"
>
+
+
+
+ {strategy.name}
+ {strategy.type}
+
+
+ {
+ modal.current?.dismiss();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {strategy.name}
+
+
+ {strategy.type}
+
+
+
+
+
+
-
- {
- modal.current?.dismiss();
- }}
- >
-
-
-
-
-
-
- {strategy.name}
-
-
- {strategy.type}
-
By exchange ETH to wstETH you will incrase your ETH holdings balance by {baseAPRstETH.toFixed(2)}% APY from staking liquidity on Lido finance.
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 0393df46..474e29d4 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -110,7 +110,6 @@ export function Header({
mode="ios"
value={currentSegment}
onIonChange={(e: any) => {
- console.log('>> router: ', router)
router.push(`/${e.detail.value}`)
if (e.detail.value === 'fiat-segment') {
handleSegmentChange({detail: {value: 'fiat'}});
@@ -119,12 +118,13 @@ export function Header({
handleSegmentChange(e);
}}
>
+ Wallet
Exchange
- Earn Interest
+ Earn interest
- Lending & Borrow
+ Lend & borrow
Buy
diff --git a/src/components/LoanFormModal.tsx b/src/components/LoanFormModal.tsx
index 6683bbc5..cb305563 100644
--- a/src/components/LoanFormModal.tsx
+++ b/src/components/LoanFormModal.tsx
@@ -62,6 +62,7 @@ export function LoanFormModal({
const readableAction =
actionType[0].toUpperCase() + actionType.slice(1).toLocaleLowerCase();
+ console.log('>>>', userSummary)
return (
diff --git a/src/components/MATICLiquidStakingstrategy.tsx b/src/components/MATICLiquidStakingstrategy.tsx
index eef85301..59eabc52 100644
--- a/src/components/MATICLiquidStakingstrategy.tsx
+++ b/src/components/MATICLiquidStakingstrategy.tsx
@@ -1,11 +1,13 @@
import {
IonAvatar,
IonButton,
+ IonButtons,
IonCard,
IonCardContent,
IonCol,
IonContent,
IonGrid,
+ IonHeader,
IonIcon,
IonImg,
IonItem,
@@ -17,6 +19,8 @@ import {
IonSkeletonText,
IonSpinner,
IonText,
+ IonTitle,
+ IonToolbar,
useIonToast,
} from "@ionic/react";
import { ethers } from "ethers";
@@ -171,7 +175,8 @@ export function MATICLiquidStakingstrategyCard(props: { asImage?: boolean, asIte
},
hiddenUI: [
...LIFI_CONFIG?.hiddenUI as any,
- HiddenUI.ToAddress
+ HiddenUI.ToAddress,
+ HiddenUI.History,
],
disabledUI: action === 'stake'
? [ "toToken"]
@@ -422,7 +427,9 @@ export function MATICLiquidStakingstrategyCard(props: { asImage?: boolean, asIte
}}>
@@ -431,8 +438,8 @@ export function MATICLiquidStakingstrategyCard(props: { asImage?: boolean, asIte
{strategy.name}
-
-
+
+
{strategy.type}
@@ -452,51 +459,75 @@ export function MATICLiquidStakingstrategyCard(props: { asImage?: boolean, asIte
}}
className="modalPage"
>
+
+
+
+ {strategy.name}
+ {strategy.type}
+
+
+ {
+ modal.current?.dismiss();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {strategy.name}
+
+
+ {strategy.type}
+
+
+
+
+
+
-
- {
- modal.current?.dismiss();
- }}
- >
-
-
-
-
-
-
- {strategy.name}
-
-
- {strategy.type}
-
By exchange MATIC to stMATIC you will incrase your MATIC holdings balance by {baseAPRst.toFixed(2)}% APY from staking liquidity on Lido finance.
diff --git a/src/components/ReserveDetail.tsx b/src/components/ReserveDetail.tsx
index a7bfb172..e2e79de5 100644
--- a/src/components/ReserveDetail.tsx
+++ b/src/components/ReserveDetail.tsx
@@ -249,6 +249,7 @@ export function ReserveDetail(props: IReserveDetailProps) {
throw new Error("No poolGroup found");
}
+ console.log('>>>>x x', userSummaryAndIncentivesGroup)
const userSummary = userSummaryAndIncentivesGroup?.find((group) =>
group.userReservesData.find(({ reserve }) => reserve.id === id)
);
diff --git a/src/components/base/WalletBaseContainer.tsx b/src/components/base/WalletBaseContainer.tsx
new file mode 100644
index 00000000..4dade31e
--- /dev/null
+++ b/src/components/base/WalletBaseContainer.tsx
@@ -0,0 +1,170 @@
+import { IonModal, ModalOptions } from "@ionic/react";
+import React from "react";
+import { IAsset } from "@/interfaces/asset.interface";
+import { DepositContainer } from "@/containers/DepositContainer";
+import { HookOverlayOptions } from "@ionic/react/dist/types/hooks/HookOverlayOptions";
+import { TransferContainer } from "../../containers/TransferContainer";
+
+export interface WalletComponentProps {
+ modalOpts: Omit &
+ HookOverlayOptions;
+ walletAddress?: string;
+ assets: IAsset[];
+}
+
+export interface WalletComponentState {
+ filterBy: string | null;
+ assetGroup: any[];
+ totalBalance: number;
+ selectedTokenDetail: {
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+ } | null;
+ isEarnModalOpen: boolean;
+ isTransferModalOpen: boolean;
+ isDepositModalOpen: boolean;
+}
+
+interface demo {
+ demo: string;
+}
+
+export default class WalletBaseComponent extends React.Component<
+ T & WalletComponentProps,
+ WalletComponentState
+> {
+ constructor(props: T & WalletComponentProps) {
+ super(props);
+ this.state = {
+ filterBy: null,
+ selectedTokenDetail: null,
+ assetGroup: [],
+ totalBalance: 0,
+ isEarnModalOpen: false,
+ isTransferModalOpen: false,
+ isDepositModalOpen: false,
+ };
+ }
+
+ componentDidMount() {
+ this.calculateBalance();
+ this.groupAssets();
+ }
+
+ componentDidUpdate(
+ prevProps: Readonly,
+ prevState: Readonly,
+ snapshot?: any
+ ): void {
+ if (prevProps.assets !== this.props.assets) {
+ this.calculateBalance();
+ this.groupAssets();
+ }
+ }
+
+ calculateBalance() {
+ if (!this.props.assets) {
+ this.setState({ totalBalance: 0 });
+ return;
+ }
+ const totalBalance = this.props.assets.reduce((acc, asset) => {
+ return acc + asset.balanceUsd;
+ }, 0);
+ this.setState({ totalBalance });
+ }
+
+ groupAssets() {
+ const assetGroup = this.props.assets
+ .toSorted((a, b) => b.balanceUsd - a.balanceUsd)
+ .reduce((acc, asset) => {
+ // check existing asset symbol
+ const symbol = (asset.name.toLowerCase().includes('aave') && asset.name.toLowerCase() !== 'aave token')
+ ? asset.name.split(' ').pop()||asset.symbol
+ : asset.symbol;
+ const name = (asset.name.toLowerCase().includes('aave') && asset.name.toLowerCase() !== 'aave token')
+ ? asset.name.split(' ').pop()||asset.name
+ : asset.name;
+
+
+ const index = acc.findIndex((a) => a.symbol === symbol);
+ if (index !== -1) {
+ const balanceUsd = (asset.balanceUsd <= 0 && asset.balance > 0 )
+ ? acc[index].priceUsd * asset.balance
+ : asset.balanceUsd;
+ acc[index].balance += asset.balance;
+ acc[index].balanceUsd += balanceUsd;
+ acc[index].assets.push(asset);
+ } else {
+ acc.push({
+ name: name,
+ symbol: symbol,
+ priceUsd: asset.priceUsd,
+ thumbnail: asset.thumbnail,
+ balance: asset.balance,
+ balanceUsd: asset.balanceUsd,
+ assets: [asset],
+ });
+ }
+ return acc;
+ }, [] as { name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[] }[])
+ this.setState({ assetGroup });
+ }
+
+ handleSearchChange(e: CustomEvent) {
+ this.setState({ filterBy: e.detail.value });
+ }
+
+ handleTokenDetailClick(token: any = null) {
+ console.log(token);
+ this.setState((prev) =>({
+ ...prev,
+ selectedTokenDetail: token
+ }));
+ }
+
+ handleEarnClick() {
+ this.setState({ isEarnModalOpen: !this.state.isEarnModalOpen });
+ }
+
+ handleTransferClick(state: boolean) {
+ console.log('handleTransferClick', state)
+ this.setState({isTransferModalOpen: state});
+ }
+
+ handleDepositClick(state?: boolean) {
+ this.setState({
+ isDepositModalOpen:
+ state !== undefined ? state : !this.state.isDepositModalOpen,
+ });
+ }
+
+ render(): React.ReactNode {
+ return (
+ <>
+ this.handleTransferClick(false)}
+ >
+
+
+
+ this.handleDepositClick(false)}
+ >
+
+
+
+ >
+ );
+ }
+}
diff --git a/src/components/mobile/ActionNavButtons.tsx b/src/components/mobile/ActionNavButtons.tsx
index 8b8b1a79..bfc459e9 100644
--- a/src/components/mobile/ActionNavButtons.tsx
+++ b/src/components/mobile/ActionNavButtons.tsx
@@ -1,12 +1,14 @@
-import { IonCol, IonFab, IonFabButton, IonIcon, IonModal, IonRow, ModalOptions, useIonModal } from "@ionic/react";
-import { MobileTransferModal } from "./MobileTransferModal";
-import { IAsset } from "@/interfaces/asset.interface";
-import { MobileDepositModal } from "./MobileDepositModal";
-import { MobileSwapModal } from "./MobileSwapModal";
-import { MobileTokenDetailModal } from "./MobileTokenDetailModal";
+import {
+ IonCol,
+ IonFab,
+ IonFabButton,
+ IonIcon,
+ IonModal,
+ IonRow,
+ ModalOptions,
+ useIonModal,
+} from "@ionic/react";
import { HookOverlayOptions } from "@ionic/react/dist/types/hooks/HookOverlayOptions";
-import { useState } from "react";
-import { MobileEarnModal } from "./MobileEarnModal";
import { paperPlane, download, repeat, card } from "ionicons/icons";
const style = {
@@ -20,31 +22,14 @@ const style = {
export const MobileActionNavButtons = (props: {
hideEarnBtn?: boolean;
- selectedTokenDetail: {
- name: string;
- symbol: string;
- priceUsd: number;
- balance: number;
- balanceUsd: number;
- thumbnail: string;
- assets: IAsset[];
- }| null;
-
+ setState: (state: any) => void;
+ setIsSwapModalOpen: () => void;
}) => {
- const { selectedTokenDetail, hideEarnBtn = false } = props;
-
- const [presentDeposit, dismissDeposit] = useIonModal(MobileDepositModal, {
- ...selectedTokenDetail,
- });
- const [presentSwap, dismissSwap] = useIonModal(MobileSwapModal, {
- ...selectedTokenDetail,
- });
- const [presentTokenDetail, dismissTokenDetail] = useIonModal(
- MobileTokenDetailModal,
- { ...selectedTokenDetail }
- );
- const [isEarnModalOpen, setIsEarnModalOpen] = useState(false);
- const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
+ const {
+ hideEarnBtn = false,
+ setState,
+ setIsSwapModalOpen,
+ } = props;
const modalOpts: Omit &
HookOverlayOptions = {
@@ -54,67 +39,54 @@ export const MobileActionNavButtons = (props: {
return (
-
-
- setIsTransferModalOpen(true)}
- >
-
-
-
- setIsTransferModalOpen(() => false)}
- >
-
-
-
-
-
- presentDeposit(modalOpts)}
- >
-
-
-
-
-
-
- presentSwap(modalOpts)}
- >
-
-
-
-
- { hideEarnBtn !== true && (
+
{
- setIsEarnModalOpen(() => true);
- }}
+ onClick={() => setState({ isTransferModalOpen: true })}
>
-
+
- setIsEarnModalOpen(() => false)}
- >
-
-
- )}
-
+
+
+ setState({ isDepositModalOpen: true })}
+ >
+
+
+
+
+
+
+
+ setIsSwapModalOpen()}
+ >
+
+
+
+
+
+ {hideEarnBtn !== true && (
+
+
+ {
+ setState({ isEarnModalOpen: true });
+ }}
+ >
+
+
+
+
+ )}
+
);
-}
\ No newline at end of file
+};
diff --git a/src/components/mobile/WalletComponent.module.css b/src/components/mobile/WalletComponent.module.css
deleted file mode 100644
index 450eebe6..00000000
--- a/src/components/mobile/WalletComponent.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.header {
- /* background: transparent; */
-}
-
-.stickyWrapper {
- position: relative;
-
-}
-.stickyElement {
- position: sticky;
- top: 0;
-}
\ No newline at end of file
diff --git a/src/components/mobile/WalletComponent.tsx b/src/components/mobile/WalletComponent.tsx
deleted file mode 100644
index 2bd48cfd..00000000
--- a/src/components/mobile/WalletComponent.tsx
+++ /dev/null
@@ -1,388 +0,0 @@
-import Store from "@/store";
-import { getWeb3State } from "@/store/selectors";
-import { getReadableValue } from "@/utils/getReadableValue";
-import {
- IonAvatar,
- IonButton,
- IonButtons,
- IonCard,
- IonCardContent,
- IonCol,
- IonContent,
- IonFab,
- IonFabButton,
- IonGrid,
- IonHeader,
- IonIcon,
- IonItem,
- IonItemOption,
- IonItemOptions,
- IonItemSliding,
- IonLabel,
- IonList,
- IonModal,
- IonPage,
- IonRow,
- IonSearchbar,
- IonText,
- IonTitle,
- IonToolbar,
- ModalOptions,
- useIonAlert,
- useIonModal,
- useIonRouter,
-} from "@ionic/react";
-import { paperPlane, download, repeat, card } from "ionicons/icons";
-import { useMemo, useState } from "react";
-import styleRule from "./WalletComponent.module.css";
-import { IAsset } from "@/interfaces/asset.interface";
-import { MobileDepositModal } from "./MobileDepositModal";
-import { HookOverlayOptions } from "@ionic/react/dist/types/hooks/HookOverlayOptions";
-import { MobileTransferModal } from "./MobileTransferModal";
-import { getMagic } from "@/servcies/magic";
-import { MobileSwapModal } from "./MobileSwapModal";
-import { MobileTokenDetailModal } from "./MobileTokenDetailModal";
-import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
-import { MobileActionNavButtons } from "./ActionNavButtons";
-
-export default function WalletComponent() {
- const { walletAddress, isMagicWallet, assets } = Store.useState(getWeb3State);
- const [presentAlert, dismissAlert] = useIonAlert();
- const [filterBy, setFilterBy] = useState(null);
- const [selectedTokenDetail, setSelectedTokenDetail] = useState<{
- name: string;
- symbol: string;
- priceUsd: number;
- balance: number;
- balanceUsd: number;
- thumbnail: string;
- assets: IAsset[];
- } | null>(null);
-
- const [presentTransfer] = useIonModal(MobileTransferModal, {
- ...selectedTokenDetail,
- });
- const [presentDeposit] = useIonModal(MobileDepositModal, {
- ...selectedTokenDetail,
- });
- const [presentSwap] = useIonModal(MobileSwapModal, {
- ...selectedTokenDetail,
- });
- const [presentTokenDetail, dismissTokenDetail] = useIonModal(
- MobileTokenDetailModal,
- { data: selectedTokenDetail, dismiss: () => {
- dismissTokenDetail();
- setSelectedTokenDetail(null);
- }}
- );
-
- const modalOpts: Omit &
- HookOverlayOptions = {
- initialBreakpoint: 0.98,
- breakpoints: [0, 0.98],
- };
- // const assets: any[] = [];
- const router = useIonRouter();
- const balance = useMemo(() => {
- if (!assets) {
- return 0;
- }
- return assets.reduce((acc, asset) => {
- return acc + asset.balanceUsd;
- }, 0);
- }, [assets]);
-
- const assetGroup = useMemo(
- () =>
- assets.reduce((acc, asset) => {
- // check existing asset symbol
- const index = acc.findIndex((a) => a.symbol === asset.symbol);
- if (index !== -1) {
- acc[index].balance += asset.balance;
- acc[index].balanceUsd += asset.balanceUsd;
- acc[index].assets.push(asset);
- } else {
- acc.push({
- name: asset.name,
- symbol: asset.symbol,
- priceUsd: asset.priceUsd,
- thumbnail: asset.thumbnail,
- balance: asset.balance,
- balanceUsd: asset.balanceUsd,
- assets: [asset],
- });
- }
- return acc;
- }, [] as { name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[] }[]),
- [assets]
- );
-
- return (
-
-
-
-
- Wallet
-
- $ {balance.toFixed(2)}
-
-
-
-
-
-
-
-
-
-
-
-
- Wallet
-
- $ {getReadableValue(balance)}
-
-
-
-
-
-
-
-
- {assetGroup.length > 0 && (
-
-
-
- {
- console.log(event);
- setFilterBy(event.detail.value || null);
- }}
- >
-
-
-
- )}
-
-
-
-
-
- {balance <= 0 && (
-
-
- {
- if (
- walletAddress &&
- walletAddress !== "" &&
- isMagicWallet
- ) {
- const magic = await getMagic();
- magic.wallet.showOnRamp();
- } else {
- await presentAlert({
- header: "Information",
- message:
- "Connect with e-mail or social login to enable buy crypto with fiat.",
- buttons: ["OK"],
- cssClass: "modalAlert",
- });
- }
- }}
- >
-
-
-
-
-
-
-
-
- Buy crypto
-
- You have to get ETH to use your wallet. Buy with
- credit card or with Apple Pay
-
-
-
-
-
-
-
-
- presentDeposit(modalOpts)}
- >
-
-
-
-
-
-
-
-
- Deposit assets
-
- You have to get ETH to use your wallet. Buy with
- credit card or with Apple Pay
-
-
-
-
-
-
-
-
-
- )}
-
- {balance > 0 && (
-
-
-
- {assetGroup
- .filter((asset) =>
- filterBy
- ? asset.symbol
- .toLowerCase()
- .includes(filterBy.toLowerCase())
- : true
- )
- .sort((a, b) => (a.balanceUsd > b.balanceUsd ? -1 : 1))
- .map((asset, index) => (
-
- {
- setSelectedTokenDetail(() => asset);
- presentTokenDetail({
- ...modalOpts,
- onDidDismiss: () => setSelectedTokenDetail(null),
- });
- }}
- >
-
- {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
-
- {asset.symbol}
-
-
-
- {asset.name}
-
-
-
-
-
- $ {asset.balanceUsd.toFixed(2)}
-
-
- {asset.balance.toFixed(6)}
-
-
-
-
-
- {
- setSelectedTokenDetail(() => asset);
- presentTransfer({
- ...modalOpts,
- onDidDismiss: () =>
- setSelectedTokenDetail(null),
- });
- }}
- >
-
-
- {
- setSelectedTokenDetail(() => asset);
- presentSwap({
- ...modalOpts,
- onDidDismiss: () =>
- setSelectedTokenDetail(null),
- });
- }}
- >
-
-
-
-
- ))}
-
-
-
- )}
-
-
-
- );
-}
diff --git a/src/components/mobile/MobileDepositModal.tsx b/src/containers/DepositContainer.tsx
similarity index 97%
rename from src/components/mobile/MobileDepositModal.tsx
rename to src/containers/DepositContainer.tsx
index 5c9a78bc..17dacfef 100644
--- a/src/components/mobile/MobileDepositModal.tsx
+++ b/src/containers/DepositContainer.tsx
@@ -17,7 +17,7 @@ import {
import { useEffect, useState } from "react";
import { scan } from 'ionicons/icons';
-export const MobileDepositModal = () => {
+export const DepositContainer = () => {
const {
web3Provider,
currentNetwork,
@@ -76,6 +76,8 @@ export const MobileDepositModal = () => {
borderRadius: "32px",
overflow: "hidden",
transform: "scale(1.1)",
+ maxWidth: '250px',
+ margin: 'auto',
}}
dangerouslySetInnerHTML={{ __html: qrCodeSVG?.outerHTML || "" }}
/>
diff --git a/src/components/mobile/MobileTransferModal.tsx b/src/containers/TransferContainer.tsx
similarity index 78%
rename from src/components/mobile/MobileTransferModal.tsx
rename to src/containers/TransferContainer.tsx
index 8cf5dbc2..902ca0b8 100644
--- a/src/components/mobile/MobileTransferModal.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -8,6 +8,7 @@ import {
IonFab,
IonFabButton,
IonGrid,
+ IonHeader,
IonIcon,
IonInput,
IonItem,
@@ -18,9 +19,10 @@ import {
IonPopover,
IonRow,
IonText,
+ IonToolbar,
} from "@ionic/react";
import { chevronDown, close, scan } from "ionicons/icons";
-import { SymbolIcon } from "../SymbolIcon";
+import { SymbolIcon } from "../components/SymbolIcon";
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from "react";
import { CHAIN_AVAILABLES, CHAIN_DEFAULT } from "@/constants/chains";
import { getReadableAmount } from "@/utils/getReadableAmount";
@@ -232,9 +234,9 @@ const InputAssetWithDropDown = (props: {
{
- setPopoverOpen(() => true);
+ setPopoverOpen(false);
setSelectedAsset(asset);
setInputFromAmount(() => 0);
setErrorMessage(() => undefined);
@@ -323,7 +325,7 @@ const InputAssetWithDropDown = (props: {
);
};
-export const MobileTransferModal = () => {
+export const TransferContainer = () => {
const { walletAddress, isMagicWallet, assets } = Store.useState(getWeb3State);
const [inputFromAmount, setInputFromAmount] = useState(0);
const [inputToAddress, setInputToAddress] = useState(undefined);
@@ -332,74 +334,85 @@ export const MobileTransferModal = () => {
const isValid = inputFromAmount > 0 && inputToAddress && inputToAddress.length > 0;
return (
-
-
-
-
-
- Send token
-
-
-
-
-
-
-
-
- Destination address
-
- {
- console.log($event)
- setInputToAddress(() => ($event.detail.value|| undefined));
- }} />
- {
- setIsScanModalOpen(()=> true);
+ <>
+
+
+
+
+
+
+ Send token
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- {
- if (data) {
- setInputToAddress(() => data);
- }
- setIsScanModalOpen(() => false);
- }} />
-
-
- Send
-
-
-
-
+
+ Destination address
+
+ {
+ console.log($event)
+ setInputToAddress(() => ($event.detail.value|| undefined));
+ }} />
+ {
+ setIsScanModalOpen(()=> true);
+ }}
+ >
+
+
+
+ {
+ if (data) {
+ setInputToAddress(() => data);
+ }
+ setIsScanModalOpen(() => false);
+ }} />
+
+
+ Send
+
+
+
+
+ >
+
);
};
diff --git a/src/containers/DefiContainer.tsx b/src/containers/desktop/DefiContainer.tsx
similarity index 98%
rename from src/containers/DefiContainer.tsx
rename to src/containers/desktop/DefiContainer.tsx
index 4629575f..7e5f7f87 100644
--- a/src/containers/DefiContainer.tsx
+++ b/src/containers/desktop/DefiContainer.tsx
@@ -17,11 +17,11 @@ import {
import { chevronDownOutline } from "ionicons/icons";
import { ChainId } from "@aave/contract-helpers";
-import { getPercent } from "../utils/utils";
-import { CHAIN_AVAILABLES } from "../constants/chains";
+import { getPercent } from "../../utils/utils";
+import { CHAIN_AVAILABLES } from "../../constants/chains";
import { useEffect, useState } from "react";
-import { MarketList } from "../components/MarketsList";
-import { currencyFormat } from "../utils/currency-format";
+import { MarketList } from "../../components/MarketsList";
+import { currencyFormat } from "../../utils/currency-format";
import { valueToBigNumber } from "@aave/math-utils";
import { getReadableValue } from "@/utils/getReadableValue";
import Store from "@/store";
@@ -47,6 +47,8 @@ export default function DefiContainer({
null
);
+ console.log("userSummaryAndIncentivesGroup>> ", userSummaryAndIncentivesGroup)
+
const totalBorrowsUsd = protocolSummary.reduce((prev, current)=> {
return prev + current.totalBorrowsUSD;
}, 0);
diff --git a/src/containers/EarnContainer.tsx b/src/containers/desktop/EarnContainer.tsx
similarity index 95%
rename from src/containers/EarnContainer.tsx
rename to src/containers/desktop/EarnContainer.tsx
index cc0459c0..a7560a56 100644
--- a/src/containers/EarnContainer.tsx
+++ b/src/containers/desktop/EarnContainer.tsx
@@ -1,6 +1,6 @@
import { IonCol, IonGrid, IonRow, IonText } from "@ionic/react";
-import { ETHLiquidStakingstrategyCard } from "../components/ETHLiquidStakingstrategy";
-import { MATICLiquidStakingstrategyCard } from "../components/MATICLiquidStakingstrategy";
+import { ETHLiquidStakingstrategyCard } from "../../components/ETHLiquidStakingstrategy";
+import { MATICLiquidStakingstrategyCard } from "../../components/MATICLiquidStakingstrategy";
import { ATOMLiquidStakingstrategyCard } from "@/components/ATOMLiquidStakingstrategy";
import { ETHsfrxLiquidStakingstrategyCard } from "@/components/ETHsfrxLiquidStakingstrategy";
import { MoreInfo } from "@/components/MoreInfo";
diff --git a/src/containers/FiatContainer.tsx b/src/containers/desktop/FiatContainer.tsx
similarity index 100%
rename from src/containers/FiatContainer.tsx
rename to src/containers/desktop/FiatContainer.tsx
diff --git a/src/containers/LeaderboardContainer.tsx b/src/containers/desktop/LeaderboardContainer.tsx
similarity index 100%
rename from src/containers/LeaderboardContainer.tsx
rename to src/containers/desktop/LeaderboardContainer.tsx
diff --git a/src/containers/SwapContainer.tsx b/src/containers/desktop/SwapContainer.tsx
similarity index 96%
rename from src/containers/SwapContainer.tsx
rename to src/containers/desktop/SwapContainer.tsx
index c780fdf8..3788df25 100644
--- a/src/containers/SwapContainer.tsx
+++ b/src/containers/desktop/SwapContainer.tsx
@@ -14,11 +14,11 @@ import {
} from "@lifi/widget";
import type { Route } from "@lifi/sdk";
import { useEffect } from "react";
-import { useLoader } from "../context/LoaderContext";
-import { CHAIN_AVAILABLES, CHAIN_DEFAULT, NETWORK } from "../constants/chains";
+import { useLoader } from "../../context/LoaderContext";
+import { CHAIN_AVAILABLES, CHAIN_DEFAULT, NETWORK } from "../../constants/chains";
import { ethers } from "ethers";
-import { LiFiWidgetDynamic } from "../components/LiFiWidgetDynamic";
-import { LIFI_CONFIG } from "../servcies/lifi.service";
+import { LiFiWidgetDynamic } from "../../components/LiFiWidgetDynamic";
+import { LIFI_CONFIG } from "../../servcies/lifi.service";
// import { SquidWidgetDynamic } from "@/components/SquidWidgetDynamic";
import { SquidWidget } from "@0xsquid/widget";
import { SQUID_CONFIG } from "@/servcies/squid.service";
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
new file mode 100644
index 00000000..3dfa3852
--- /dev/null
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -0,0 +1,326 @@
+import { card, download, paperPlane } from "ionicons/icons";
+import WalletBaseComponent, { WalletComponentProps } from "../../components/base/WalletBaseContainer";
+import {
+ IonAvatar,
+ IonButton,
+ IonCard,
+ IonCardContent,
+ IonCol,
+ IonGrid,
+ IonIcon,
+ IonLabel,
+ IonRow,
+ IonText,
+} from "@ionic/react";
+import ConnectButton from "@/components/ConnectButton";
+import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+
+class WalletDesktopContainer extends WalletBaseComponent {
+ constructor(props: WalletComponentProps) {
+ super(props);
+ }
+
+ render() {
+ return (
+ <>
+ {super.render()}
+
+
+
+
+ Wallet
+
+
+
+ $ {this.state.totalBalance.toFixed(2)}
+
+
+
+
+ {/* send btn + Deposit btn */}
+ {
+ this.handleTransferClick(true);
+ }}
+ >
+
+ Send
+
+ {
+ this.handleDepositClick();
+ }}
+ >
+
+ Deposit
+
+
+
+
+ {!this.props.walletAddress && (
+
+
+
+
+ Connecting your wallet is key to accessing a snapshot of your
+ assets.
+ It grants you direct insight into your holdings and balances.
+
+
+
+
+
+
+
+ )}
+
+ {(this.props.walletAddress && this.state.assetGroup.length === 0) && (
+
+
+ {}}
+ >
+
+
+
+
+
+
+
+
+ Buy crypto
+
+ You have to get ETH to use your wallet. Buy with
+ credit card or with Apple Pay
+
+
+
+
+
+
+
+
+
+ {}}
+ >
+
+
+
+
+
+
+
+
+ Deposit assets
+
+ Transfer tokens from another wallet or from a crypto
+ exchange
+
+
+
+
+
+
+
+
+
+
+
+ You are using a non-custodial wallet that give you complete
+ control over your cryptocurrency funds and private keys.
+ Unlike custodial wallets, you manage your own security,
+ enhancing privacy and independence in the decentralized
+ cryptocurrency space.
+
+
+
+
+ )}
+
+ {/* wrapper to display card with assets items */}
+ {this.state.assetGroup.length > 0 && (
+
+
+
+
+
+
+ Asset
+
+
+
+
+ Price
+
+
+ Balance
+
+
+ Value
+
+
+
+
+ {this.state.assetGroup.map((asset, index) => {
+ return (
+ {
+ console.log("asset", asset);
+ }}
+ style={{
+ borderBottom:
+ "solid 1px rgba(var(--ion-color-primary-rgb), 0.2)",
+ }}
+ >
+
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {asset.symbol}
+
+ {asset.name}
+
+
+
+
+
+
+ $ {asset.priceUsd.toFixed(2)}
+
+
+
+
+ {asset.balance.toFixed(2)}
+
+
+
+
+ $ {asset.balanceUsd.toFixed(2)}
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ >
+ );
+ }
+}
+
+const withStore = (Component: React.ComponentClass) => {
+
+ // use named function to prevent re-rendering failure
+ return function WalletDesktopContainerWithStore() {
+ const { walletAddress, assets } =
+ Store.useState(getWeb3State);
+
+ return (
+
+ );
+ };
+};
+export default withStore(WalletDesktopContainer);
\ No newline at end of file
diff --git a/src/components/mobile/MobileEarnModal.tsx b/src/containers/mobile/EarnMobileContainer.tsx
similarity index 96%
rename from src/components/mobile/MobileEarnModal.tsx
rename to src/containers/mobile/EarnMobileContainer.tsx
index 094df6b8..1aadf67b 100644
--- a/src/components/mobile/MobileEarnModal.tsx
+++ b/src/containers/mobile/EarnMobileContainer.tsx
@@ -15,7 +15,7 @@ import {
IonToolbar,
} from "@ionic/react";
import { useEffect, useMemo, useState } from "react";
-import { MarketList } from "../MarketsList";
+import { MarketList } from "../../components/MarketsList";
import {
initializePools,
initializeUserSummary,
@@ -29,10 +29,10 @@ import {
import { patchPoolsState } from "@/store/actions";
import { CHAIN_AVAILABLES } from "@/constants/chains";
import { getReadableValue } from "@/utils/getReadableValue";
-import { ETHLiquidStakingstrategyCard } from "../ETHLiquidStakingstrategy";
-import { MATICLiquidStakingstrategyCard } from "../MATICLiquidStakingstrategy";
+import { ETHLiquidStakingstrategyCard } from "../../components/ETHLiquidStakingstrategy";
+import { MATICLiquidStakingstrategyCard } from "../../components/MATICLiquidStakingstrategy";
-export const MobileEarnModal = () => {
+export const EarnMobileContainer = () => {
const [segment, setSegment] = useState("loan");
const { walletAddress } = Store.useState(getWeb3State);
const userSummaryAndIncentivesGroup = Store.useState(
diff --git a/src/components/mobile/MobileSwapModal.tsx b/src/containers/mobile/SwapMobileContainer.tsx
similarity index 92%
rename from src/components/mobile/MobileSwapModal.tsx
rename to src/containers/mobile/SwapMobileContainer.tsx
index bc22b0a5..97d7fa26 100644
--- a/src/components/mobile/MobileSwapModal.tsx
+++ b/src/containers/mobile/SwapMobileContainer.tsx
@@ -20,19 +20,21 @@ import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { CHAIN_DEFAULT } from "@/constants/chains";
import { ethers } from "ethers";
-import { LiFiWidgetDynamic } from "../LiFiWidgetDynamic";
+import { LiFiWidgetDynamic } from "../../components/LiFiWidgetDynamic";
import { useLoader } from "@/context/LoaderContext";
import { LIFI_CONFIG } from "../../servcies/lifi.service";
import { IAsset } from "@/interfaces/asset.interface";
-export const MobileSwapModal = (props?: {
- name: string;
- symbol: string;
- priceUsd: number;
- balance: number;
- balanceUsd: number;
- thumbnail: string;
- assets: IAsset[];
+export const SwapMobileContainer = (props: {
+ token?: {
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+ }
}) => {
const {
web3Provider,
@@ -163,12 +165,12 @@ export const MobileSwapModal = (props?: {
signer,
},
// set source chain to Polygon
- fromChain: props?.assets?.[0]?.chain?.id || CHAIN_DEFAULT.id,
+ fromChain: props?.token?.assets?.[0]?.chain?.id || CHAIN_DEFAULT.id,
// set destination chain to Optimism
toChain: currentNetwork || CHAIN_DEFAULT.id,
// set source token to ETH (Ethereum)
fromToken:
- props?.assets?.[0]?.contractAddress ||
+ props?.token?.assets?.[0]?.contractAddress ||
"0x0000000000000000000000000000000000000000",
};
diff --git a/src/components/mobile/MobileTokenDetailModal.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
similarity index 83%
rename from src/components/mobile/MobileTokenDetailModal.tsx
rename to src/containers/mobile/TokenDetailMobileContainer.tsx
index c7f57337..eb53a019 100644
--- a/src/components/mobile/MobileTokenDetailModal.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -1,7 +1,7 @@
import { IAsset } from "@/interfaces/asset.interface";
import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
import { IonAvatar, IonCol, IonContent, IonGrid, IonItem, IonLabel, IonList, IonListHeader, IonNote, IonRow, IonText } from "@ionic/react"
-import { MobileActionNavButtons } from "./ActionNavButtons";
+import { MobileActionNavButtons } from "../../components/mobile/ActionNavButtons";
import { useEffect } from "react";
import { ethers } from "ethers";
import Store from "@/store";
@@ -13,9 +13,11 @@ const getTxsFromAddress = async (address: string) => {
let history = await provider.getHistory(address);
console.log(history);
}
-export const MobileTokenDetailModal = (props: {
+export const TokenDetailMobileContainer = (props: {
data: {name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[]};
dismiss: () => void;
+ setState: (state: any) => void;
+ setIsSwapModalOpen: (state: boolean) => void;
})=> {
const { data, dismiss } = props;
const { walletAddress } = Store.useState(getWeb3State);
@@ -63,7 +65,12 @@ export const MobileTokenDetailModal = (props: {
-
+
+ props.setState(state)}
+ setIsSwapModalOpen={() => props.setIsSwapModalOpen(true)}
+ hideEarnBtn={true} />
+
Networks
- {data.assets.map((token, index) =>
+ {data.assets
+ .sort((a, b) => a.chain && b.chain
+ ? a.chain.id - b.chain.id
+ : a.balance + b.balance
+ )
+ .map((token, index) =>
-
+
c.id === token.chain?.id)?.logo}
alt={token.symbol}
@@ -92,7 +104,7 @@ export const MobileTokenDetailModal = (props: {
/>
- {token.chain?.name}
+ {token.chain?.name}
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
new file mode 100644
index 00000000..b011b09a
--- /dev/null
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -0,0 +1,422 @@
+import Store from "@/store";
+import WalletBaseComponent, {
+ WalletComponentProps,
+} from "../../components/base/WalletBaseContainer";
+import { getWeb3State } from "@/store/selectors";
+import { MobileActionNavButtons } from "@/components/mobile/ActionNavButtons";
+import { getMagic } from "@/servcies/magic";
+import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
+import { getReadableValue } from "@/utils/getReadableValue";
+import {
+ IonPage,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+ IonGrid,
+ IonRow,
+ IonCol,
+ IonText,
+ IonSearchbar,
+ IonCard,
+ IonCardContent,
+ IonIcon,
+ IonList,
+ IonItemSliding,
+ IonItem,
+ IonAvatar,
+ IonLabel,
+ IonItemOptions,
+ IonItemOption,
+ IonAlert,
+ IonModal,
+} from "@ionic/react";
+import { card, download, paperPlane, repeat } from "ionicons/icons";
+import { useState } from "react";
+import { IAsset } from "@/interfaces/asset.interface";
+import { SwapMobileContainer } from "@/containers/mobile/SwapMobileContainer";
+import { TokenDetailMobileContainer } from "@/containers/mobile/TokenDetailMobileContainer";
+import { EarnMobileContainer } from "@/containers/mobile/EarnMobileContainer";
+
+type SelectedTokenDetail =
+ | {
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+ }
+ | undefined;
+
+interface WalletMobileComProps {
+ isMagicWallet: boolean;
+ isSwapModalOpen: SelectedTokenDetail | boolean | undefined;
+ setIsSwapModalOpen: (
+ value?: SelectedTokenDetail | boolean | undefined
+ ) => void;
+ isAlertOpen: boolean;
+ setIsAlertOpen: (value: boolean) => void;
+}
+
+class WalletMobileContainer extends WalletBaseComponent<
+ WalletComponentProps & WalletMobileComProps
+> {
+ constructor(props: WalletComponentProps & WalletMobileComProps) {
+ super(props);
+ }
+
+ render() {
+ return (
+ <>
+
+
+
+
+ Wallet
+
+ $ {this.state.totalBalance.toFixed(2)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wallet
+
+ $ {getReadableValue(this.state.totalBalance)}
+
+
+
+
+
+
+ this.setState(state)}
+ setIsSwapModalOpen={() =>
+ this.props.setIsSwapModalOpen(true)
+ }
+ />
+
+ {this.state.assetGroup.length > 0 && (
+
+
+
+ {
+ console.log(event);
+ this.setState({
+ filterBy: event.detail.value || null,
+ });
+ }}
+ >
+
+
+
+ )}
+
+
+
+
+
+ {this.state.totalBalance <= 0 && (
+
+
+ {
+ if (
+ this.props.walletAddress &&
+ this.props.walletAddress !== "" &&
+ this.props.isMagicWallet
+ ) {
+ const magic = await getMagic();
+ magic.wallet.showOnRamp();
+ } else {
+ this.props.setIsAlertOpen(false);
+ }
+ }}
+ >
+
+ this.props.setIsAlertOpen(false)
+ }
+ >
+
+
+
+
+
+
+
+
+ Buy crypto
+
+ You have to get ETH to use your wallet. Buy
+ with credit card or with Apple Pay
+
+
+
+
+
+
+
+
+ this.handleDepositClick()}
+ >
+
+
+
+
+
+
+
+
+ Deposit assets
+
+ You have to get ETH to use your wallet. Buy
+ with credit card or with Apple Pay
+
+
+
+
+
+
+
+
+
+ )}
+
+ {this.state.totalBalance > 0 && (
+
+
+
+ {this.state.assetGroup
+ .filter((asset) =>
+ this.state.filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(this.state.filterBy.toLowerCase())
+ : true
+ )
+ .sort((a, b) => (a.balanceUsd > b.balanceUsd ? -1 : 1))
+ .map((asset, index) => (
+
+ {
+ console.log("handleTokenDetailClick: ", asset);
+ this.handleTokenDetailClick(asset);
+ }}
+ >
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+
+
+ {asset.symbol}
+
+
+
+
+ {asset.name}
+
+
+
+
+
+ $ {asset.balanceUsd.toFixed(2)}
+
+
+ {asset.balance.toFixed(6)}
+
+
+
+
+ {
+ // close the sliding item after clicking the option
+ (event.target as HTMLElement)
+ .closest("ion-item-sliding")
+ ?.close();
+ }}
+ >
+ {
+ this.handleTransferClick(true);
+ }}
+ >
+
+
+ {
+ this.props.setIsSwapModalOpen(asset);
+ }}
+ >
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+ this.setState({ isEarnModalOpen: false })}
+ >
+
+
+
+ {
+ this.props.setIsSwapModalOpen(undefined);
+ }}
+ >
+
+
+
+ this.handleTokenDetailClick(null)}
+ >
+ {this.state.selectedTokenDetail && (
+ this.setState(state)}
+ setIsSwapModalOpen={() => this.props.setIsSwapModalOpen(true)}
+ data={this.state.selectedTokenDetail}
+ dismiss={() => this.handleTokenDetailClick(null)}
+ />
+ )}
+
+
+ {super.render()}
+ >
+ );
+ }
+}
+
+const withStore = (
+ Component: React.ComponentClass
+) => {
+ // use named function to prevent re-rendering failure
+ return function WalletMobileContainerWithStore() {
+ const { walletAddress, assets, isMagicWallet } =
+ Store.useState(getWeb3State);
+ const [isAlertOpen, setIsAlertOpen] = useState(false);
+ const [isSwapModalOpen, setIsSwapModalOpen] = useState<
+ SelectedTokenDetail | boolean | undefined
+ >(undefined);
+
+ return (
+ setIsSwapModalOpen(value)}
+ modalOpts={{
+ initialBreakpoint: 0.98,
+ breakpoints: [0, 0.98],
+ }}
+ />
+ );
+ };
+};
+export default withStore(WalletMobileContainer);
diff --git a/src/components/mobile/WelcomeComponent.tsx b/src/containers/mobile/WelcomeMobileContainer.tsx
similarity index 74%
rename from src/components/mobile/WelcomeComponent.tsx
rename to src/containers/mobile/WelcomeMobileContainer.tsx
index ab11c17a..8f34b968 100644
--- a/src/components/mobile/WelcomeComponent.tsx
+++ b/src/containers/mobile/WelcomeMobileContainer.tsx
@@ -1,8 +1,11 @@
import { IonCol, IonContent, IonGrid, IonImg, IonRow, IonText, useIonRouter } from "@ionic/react";
import { IonPage, } from '@ionic/react';
-import ConnectButton from "../ConnectButton";
+import ConnectButton from "../../components/ConnectButton";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
-export default function MobileWelcomeComponent() {
+export default function WelcomeMobileContainer() {
+ const { walletAddress } = Store.useState(getWeb3State);
const router = useIonRouter();
return (
@@ -28,7 +31,9 @@ export default function MobileWelcomeComponent() {
Build your wealth with cryptoassets
-
+ {!walletAddress && (
+
+ )}
From e1a93b1662ab2480328fe16f354ef2db283730c6 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Wed, 13 Mar 2024 14:36:01 +0100
Subject: [PATCH 08/74] refactor: add LightChat to TokenDetailMobile
---
package-lock.json | 14 +
package.json | 1 +
src/components/MarketsList.tsx | 4 +-
src/components/PoolAccordionGroup.tsx | 4 +-
src/components/PoolItem.tsx | 9 +-
src/components/ui/LightChart.tsx | 85 +++++
src/containers/TransferContainer.tsx | 7 +-
.../desktop/WalletDesktopContainer.tsx | 300 ++++++++++--------
.../mobile/TokenDetailMobileContainer.tsx | 246 ++++++++------
.../mobile/WalletMobileContainer.tsx | 9 +-
src/styles/global.scss | 17 +-
11 files changed, 447 insertions(+), 249 deletions(-)
create mode 100644 src/components/ui/LightChart.tsx
diff --git a/package-lock.json b/package-lock.json
index d2bff43c..64ebf5e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,6 +45,7 @@
"firebase": "^10.7.1",
"html5-qrcode": "^2.3.8",
"ionicons": "latest",
+ "lightweight-charts": "^4.1.3",
"lru-cache": "^10.1.0",
"magic-sdk": "^21.4.0",
"next": "14.0.3",
@@ -15700,6 +15701,11 @@
"node": "> 0.1.90"
}
},
+ "node_modules/fancy-canvas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz",
+ "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ=="
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -19737,6 +19743,14 @@
"immediate": "~3.0.5"
}
},
+ "node_modules/lightweight-charts": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.1.3.tgz",
+ "integrity": "sha512-SJacmEyx3LmT2Qsc7Kq7cEX7nEHtQv0MOlujhRlcDxhW62pG6nkBlcM52/jNqkq8B28KQeVmgOQ7zrdJ4BCPDw==",
+ "dependencies": {
+ "fancy-canvas": "2.1.0"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
diff --git a/package.json b/package.json
index df9d5907..a09a87bb 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"firebase": "^10.7.1",
"html5-qrcode": "^2.3.8",
"ionicons": "latest",
+ "lightweight-charts": "^4.1.3",
"lru-cache": "^10.1.0",
"magic-sdk": "^21.4.0",
"next": "14.0.3",
diff --git a/src/components/MarketsList.tsx b/src/components/MarketsList.tsx
index e0fd8e6b..f4a5fbb2 100644
--- a/src/components/MarketsList.tsx
+++ b/src/components/MarketsList.tsx
@@ -255,7 +255,7 @@ export function MarketList(props: {
}
}}
/>
-
+
{groups.map((poolGroup, index) => (
)}
{groups.length === 0 && totalTVL && (
-
+
diff --git a/src/components/PoolAccordionGroup.tsx b/src/components/PoolAccordionGroup.tsx
index 4e696ad9..5453625d 100644
--- a/src/components/PoolAccordionGroup.tsx
+++ b/src/components/PoolAccordionGroup.tsx
@@ -133,7 +133,7 @@ export function PoolAccordionGroup(props: IPoolAccordionProps) {
size-md="2"
class="ion-text-end ion-hide-sm-down"
>
-
+
{poolGroup.topSupplyApy * 100 === 0
? "0"
: poolGroup.topSupplyApy * 100 < 0.01
@@ -143,7 +143,7 @@ export function PoolAccordionGroup(props: IPoolAccordionProps) {
-
+
{poolGroup?.topBorrowApy * 100 === 0
? poolGroup?.borrowingEnabled === false
? "- "
diff --git a/src/components/PoolItem.tsx b/src/components/PoolItem.tsx
index c47b370f..75b6e441 100644
--- a/src/components/PoolItem.tsx
+++ b/src/components/PoolItem.tsx
@@ -49,14 +49,15 @@ const ActionBtn = (props: {provider: string}) => {
const { provider } = props;
if (provider === 'aave-v3') {
return (
-
-
-
+
+
)
}
return (
diff --git a/src/components/ui/LightChart.tsx b/src/components/ui/LightChart.tsx
new file mode 100644
index 00000000..b8e2a1e9
--- /dev/null
+++ b/src/components/ui/LightChart.tsx
@@ -0,0 +1,85 @@
+import { ColorType, IChartApi, createChart } from 'lightweight-charts';
+import { useEffect, useRef } from 'react';
+
+// Generated by https://quicktype.io
+
+interface DataItem {
+ time: string;
+ value: number;
+}
+
+export default function LightChart(props: { data: DataItem[] }) {
+ const chartContainerRef = useRef(null);
+ const chartRef = useRef(null);
+
+
+ useEffect(() => {
+ if (chartContainerRef.current) {
+ // remove the chart if it already exists
+ if (chartRef.current) {
+ chartRef.current.remove();
+ chartRef.current = null;
+ }
+
+ // create a new chart
+ const chart = createChart(chartContainerRef.current, {
+ width: 400,
+ height: 250,
+ layout: {
+ background: {
+ type: ColorType.Solid,
+ color: 'transparent',
+ },
+ textColor: '#d1d4dc',
+ },
+ grid: {
+ vertLines: {
+ visible: false,
+ },
+ horzLines: {
+ color: 'rgba(42, 46, 57, 0.5)',
+ },
+ },
+ rightPriceScale: {
+ borderVisible: false,
+ visible: false,
+ },
+ timeScale: {
+ borderVisible: false,
+ },
+ crosshair: {
+ horzLine: {
+ // visible: false,
+ style: 4,
+
+ },
+ },
+ });
+ // const lineSeries = chart.addLineSeries();
+ // lineSeries.setData(props.data);
+
+ const series = chart.addAreaSeries({
+ topColor: 'rgba(0,144,255, 0.618)',
+ bottomColor: 'rgba(0,144,255, 0.01)',
+ lineColor: 'rgba(0,144,255, 1)',
+ lineWidth: 3,
+ });
+ series.setData(props.data);
+
+ chart.timeScale().fitContent();
+
+ // store the chart instance in the ref
+ chartRef.current = chart;
+ }
+ // clean up the chart when the component is unmounted
+ return () => {
+ if (chartRef.current) {
+ chartRef.current.remove();
+ chartRef.current = null;
+ }
+ };
+ }, [props.data]);
+
+ return
;
+};
+
diff --git a/src/containers/TransferContainer.tsx b/src/containers/TransferContainer.tsx
index 902ca0b8..43beff85 100644
--- a/src/containers/TransferContainer.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -123,11 +123,12 @@ const ScanModal = (props: { isOpen: boolean, onDismiss: (address?: string) => vo
);
}}
onDidDismiss={()=> props.onDismiss()}>
-
+
+
- props.onDismiss()}>
+ props.onDismiss()}>
-
+
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
index 3dfa3852..01cb08ec 100644
--- a/src/containers/desktop/WalletDesktopContainer.tsx
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -1,5 +1,7 @@
import { card, download, paperPlane } from "ionicons/icons";
-import WalletBaseComponent, { WalletComponentProps } from "../../components/base/WalletBaseContainer";
+import WalletBaseComponent, {
+ WalletComponentProps,
+} from "../../components/base/WalletBaseContainer";
import {
IonAvatar,
IonButton,
@@ -10,12 +12,16 @@ import {
IonIcon,
IonLabel,
IonRow,
+ IonSearchbar,
+ IonSelect,
+ IonSelectOption,
IonText,
} from "@ionic/react";
import ConnectButton from "@/components/ConnectButton";
import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
+import { CHAIN_AVAILABLES } from "@/constants/chains";
class WalletDesktopContainer extends WalletBaseComponent {
constructor(props: WalletComponentProps) {
@@ -28,7 +34,11 @@ class WalletDesktopContainer extends WalletBaseComponent {
{super.render()}
{
{/* send btn + Deposit btn */}
{
this.handleTransferClick(true);
}}
>
-
- Send
+
+ Send
{
this.handleDepositClick();
}}
>
-
- Deposit
+
+ Deposit
@@ -79,9 +91,10 @@ class WalletDesktopContainer extends WalletBaseComponent {
- Connecting your wallet is key to accessing a snapshot of your
- assets.
- It grants you direct insight into your holdings and balances.
+ Connecting your wallet is key to accessing a snapshot of
+ your assets.
+ It grants you direct insight into your holdings and
+ balances.
@@ -91,7 +104,7 @@ class WalletDesktopContainer extends WalletBaseComponent {
)}
- {(this.props.walletAddress && this.state.assetGroup.length === 0) && (
+ {this.props.walletAddress && this.state.assetGroup.length === 0 && (
{
Deposit assets
- Transfer tokens from another wallet or from a crypto
- exchange
+ Transfer tokens from another wallet or from a
+ crypto exchange
@@ -181,125 +194,148 @@ class WalletDesktopContainer extends WalletBaseComponent {
{/* wrapper to display card with assets items */}
{this.state.assetGroup.length > 0 && (
-
-
-
-
-
-
- Asset
-
-
-
-
- Price
-
-
+
+
+ {
+ this.handleSearchChange(e);
+ }} />
+
+
+
+
+
+
+
+
+ Asset
+
+
+
- Balance
-
-
- Value
-
-
-
-
- {this.state.assetGroup.map((asset, index) => {
- return (
- {
- console.log("asset", asset);
- }}
- style={{
- borderBottom:
- "solid 1px rgba(var(--ion-color-primary-rgb), 0.2)",
- }}
- >
-
-
+ Price
+
+
-
+
+ Value
+
+
+
+
+ {this.state.assetGroup
+ .filter((asset) =>
+ this.state.filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(this.state.filterBy.toLowerCase())
+ : true
+ )
+ .map((asset, index) => {
+ return (
+ {
+ console.log("asset", asset);
+ }}
+ style={{
+ borderBottom:
+ "solid 1px rgba(var(--ion-color-primary-rgb), 0.2)",
+ }}
+ >
+
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {asset.symbol}
+
+ {asset.name}
+
+
+
+
- {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
- {asset.symbol}
-
- {asset.name}
-
-
-
-
-
-
- $ {asset.priceUsd.toFixed(2)}
-
-
-
-
- {asset.balance.toFixed(2)}
-
-
-
-
- $ {asset.balanceUsd.toFixed(2)}
-
-
-
-
-
- );
- })}
-
-
+
+
+ $ {asset.priceUsd.toFixed(2)}
+
+
+
+
+ {asset.balance.toFixed(2)}
+
+
+
+
+ $ {asset.balanceUsd.toFixed(2)}
+
+
+
+
+
+ );
+ })}
+
+
+ >
)}
>
@@ -308,19 +344,13 @@ class WalletDesktopContainer extends WalletBaseComponent {
}
const withStore = (Component: React.ComponentClass) => {
-
// use named function to prevent re-rendering failure
return function WalletDesktopContainerWithStore() {
- const { walletAddress, assets } =
- Store.useState(getWeb3State);
+ const { walletAddress, assets } = Store.useState(getWeb3State);
return (
-
+
);
};
};
-export default withStore(WalletDesktopContainer);
\ No newline at end of file
+export default withStore(WalletDesktopContainer);
diff --git a/src/containers/mobile/TokenDetailMobileContainer.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
index eb53a019..50b87b66 100644
--- a/src/containers/mobile/TokenDetailMobileContainer.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -1,12 +1,15 @@
import { IAsset } from "@/interfaces/asset.interface";
import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
-import { IonAvatar, IonCol, IonContent, IonGrid, IonItem, IonLabel, IonList, IonListHeader, IonNote, IonRow, IonText } from "@ionic/react"
+import { IonAvatar, IonButton, IonCol, IonContent, IonFooter, IonGrid, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonNote, IonRow, IonText, IonToolbar } from "@ionic/react"
import { MobileActionNavButtons } from "../../components/mobile/ActionNavButtons";
-import { useEffect } from "react";
+import { Suspense, lazy, useEffect } from "react";
import { ethers } from "ethers";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { CHAIN_AVAILABLES } from "@/constants/chains";
+import { airplane, download, paperPlane } from "ionicons/icons";
+
+const LightChart = lazy(() => import("@/components/ui/LightChart"));
const getTxsFromAddress = async (address: string) => {
let provider = new ethers.providers.EtherscanProvider();
@@ -28,98 +31,159 @@ export const TokenDetailMobileContainer = (props: {
}, [walletAddress]);
return (
-
-
-
-
-
- {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${data.symbol}&bgColor=%23000000&textColor=%23182449`;
+ <>
+
+
+
+
+
-
-
-
- {data.balance.toFixed(6)} {data.symbol}
-
-
-
-
- $ {data.balanceUsd.toFixed(2)}
-
-
-
-
+ >
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${data.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+
+ {data.balance.toFixed(6)} {data.symbol}
+
+
+
+
+ $ {data.balanceUsd.toFixed(2)}
+
+
+
+
- props.setState(state)}
- setIsSwapModalOpen={() => props.setIsSwapModalOpen(true)}
- hideEarnBtn={true} />
+
+
+ .....>} >
+
+
+
+ 1 {data.symbol} = $ {data.priceUsd}
+
+
+
+
+
-
-
-
-
-
- Networks
-
-
- {data.assets
- .sort((a, b) => a.chain && b.chain
- ? a.chain.id - b.chain.id
- : a.balance + b.balance
- )
- .map((token, index) =>
-
+
+
-
- c.id === token.chain?.id)?.logo}
- alt={token.symbol}
- style={{ transform: "scale(1.01)"}}
- onError={(event) => {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
- {token.chain?.name}
-
-
-
- {token.balance.toFixed(6)} {token.symbol}
-
- $ {token.balanceUsd.toFixed(2)}
-
-
-
-
- )}
-
-
-
-
-
+
+
+ Networks
+
+
+ {data.assets
+ .sort((a, b) => a.chain && b.chain
+ ? a.chain.id - b.chain.id
+ : a.balance + b.balance
+ )
+ .map((token, index) =>
+
+
+ c.id === token.chain?.id)?.logo}
+ alt={token.symbol}
+ style={{ transform: "scale(1.01)"}}
+ onError={(event) => {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {token.chain?.name}
+
+
+
+ {token.balance.toFixed(6)} {token.symbol}
+
+ $ {token.balanceUsd.toFixed(2)}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ props.setState({ isTransferModalOpen: true });
+ }} >
+
+ Send
+
+
+
+ {
+ props.setState({ isDepositModalOpen: true });
+ }} >
+
+ Deposit
+
+
+
+
+
+ {/* props.setState(state)}
+ setIsSwapModalOpen={() => props.setIsSwapModalOpen(true)}
+ hideEarnBtn={true} /> */}
+
+
+ >
);
}
\ No newline at end of file
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
index b011b09a..22afad89 100644
--- a/src/containers/mobile/WalletMobileContainer.tsx
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -134,10 +134,7 @@ class WalletMobileContainer extends WalletBaseComponent<
{
- console.log(event);
- this.setState({
- filterBy: event.detail.value || null,
- });
+ this.handleSearchChange(event);
}}
>
@@ -412,8 +409,8 @@ const withStore = (
isSwapModalOpen={isSwapModalOpen}
setIsSwapModalOpen={(value) => setIsSwapModalOpen(value)}
modalOpts={{
- initialBreakpoint: 0.98,
- breakpoints: [0, 0.98],
+ initialBreakpoint: 1,
+ breakpoints: [0, 1],
}}
/>
);
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 21c28291..951f05b9 100755
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -200,9 +200,13 @@ ion-alert.modalAlert {
}
+ion-accordion.accordion-expanding > [slot=header] .ion-accordion-toggle-icon,
+ion-accordion.accordion-expanded > [slot=header] .ion-accordion-toggle-icon {
+ transform: rotate(180deg) scale(0.6);
+}
.ion-accordion-toggle-icon {
color: var(--ion-color-primary);
- transform: scale(0.8);
+ transform: scale(0.6);
}
ion-accordion {
@@ -749,8 +753,9 @@ div.MuiScopedCssBaseline-root {
.mobileConentModal{
--background: var(--ion-background-color);
}
-// .reader-scan-element {
-// video {
-// height: 100%;
-// }
-// }
\ No newline at end of file
+#reader-scan-element {
+ video {
+ height: 100%;
+ width: auto!important;
+ }
+}
\ No newline at end of file
From e17e3728734d1a89981e32170d24db5cd09cd42c Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Fri, 15 Mar 2024 11:55:27 +0100
Subject: [PATCH 09/74] refactor: add token info on detail page
---
package-lock.json | 15 +
package.json | 2 +
src/components/ui/LightChart.tsx | 10 +-
src/components/ui/MoonpayOnramp.tsx | 57 ++
.../mobile/TokenDetailMobileContainer.tsx | 566 ++++++++++++++----
.../mobile/WalletMobileContainer.tsx | 16 +-
src/utils/getCoingeekoTokenId.ts | 15 +
src/utils/getTokenHistoryPrice.ts | 33 +
src/utils/getTokenInfo.ts | 71 +++
9 files changed, 649 insertions(+), 136 deletions(-)
create mode 100644 src/components/ui/MoonpayOnramp.tsx
create mode 100644 src/utils/getCoingeekoTokenId.ts
create mode 100644 src/utils/getTokenHistoryPrice.ts
create mode 100644 src/utils/getTokenInfo.ts
diff --git a/package-lock.json b/package-lock.json
index 64ebf5e0..64467e18 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,8 @@
"@magic-ext/cosmos": "^16.4.0",
"@magic-ext/polkadot": "^16.4.0",
"@magic-ext/solana": "^18.2.0",
+ "@moonpay/moonpay-js": "^0.5.0",
+ "@moonpay/moonpay-react": "^1.6.1",
"@solana/web3.js": "^1.87.6",
"@solendprotocol/solend-sdk": "^0.7.6",
"@types/jest": "^26.0.20",
@@ -5385,6 +5387,19 @@
"node": ">= 10"
}
},
+ "node_modules/@moonpay/moonpay-js": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@moonpay/moonpay-js/-/moonpay-js-0.5.0.tgz",
+ "integrity": "sha512-Q//d9kfGEQYOAxHIdXvnDrBONMR1uc2b/R48UP8uM//9f6tmUOOe5wXKnWJtK1Fh1/w3EDzigtYf8FNFiSco/w=="
+ },
+ "node_modules/@moonpay/moonpay-react": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@moonpay/moonpay-react/-/moonpay-react-1.6.1.tgz",
+ "integrity": "sha512-v2cx7W1ESrvzBf8Wj1If+poTiTX7OwZYUY0PLfDMuiCnu+di++GyI+R6VeAWUaH20KAQGiD54pFaoE7naNZrew==",
+ "peerDependencies": {
+ "react": ">=16"
+ }
+ },
"node_modules/@motionone/animation": {
"version": "10.16.3",
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.16.3.tgz",
diff --git a/package.json b/package.json
index a09a87bb..f70c4e99 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,8 @@
"@magic-ext/cosmos": "^16.4.0",
"@magic-ext/polkadot": "^16.4.0",
"@magic-ext/solana": "^18.2.0",
+ "@moonpay/moonpay-js": "^0.5.0",
+ "@moonpay/moonpay-react": "^1.6.1",
"@solana/web3.js": "^1.87.6",
"@solendprotocol/solend-sdk": "^0.7.6",
"@types/jest": "^26.0.20",
diff --git a/src/components/ui/LightChart.tsx b/src/components/ui/LightChart.tsx
index b8e2a1e9..58243481 100644
--- a/src/components/ui/LightChart.tsx
+++ b/src/components/ui/LightChart.tsx
@@ -3,7 +3,7 @@ import { useEffect, useRef } from 'react';
// Generated by https://quicktype.io
-interface DataItem {
+export interface DataItem {
time: string;
value: number;
}
@@ -14,7 +14,7 @@ export default function LightChart(props: { data: DataItem[] }) {
useEffect(() => {
- if (chartContainerRef.current) {
+ if (chartContainerRef.current && props.data.length > 0) {
// remove the chart if it already exists
if (chartRef.current) {
chartRef.current.remove();
@@ -66,7 +66,11 @@ export default function LightChart(props: { data: DataItem[] }) {
});
series.setData(props.data);
- chart.timeScale().fitContent();
+ const now = new Date();
+ chart.timeScale().setVisibleRange({
+ from: new Date(`${now.getFullYear()}-${(now.getMonth()+1) < 10 ? `0${(now.getMonth() + 1)}` : now.getMonth()+1}-${now.getDate()-7}`).getTime() / 1000 as any,
+ to: now.getTime() / 1000 as any,
+ });
// store the chart instance in the ref
chartRef.current = chart;
diff --git a/src/components/ui/MoonpayOnramp.tsx b/src/components/ui/MoonpayOnramp.tsx
new file mode 100644
index 00000000..646908ec
--- /dev/null
+++ b/src/components/ui/MoonpayOnramp.tsx
@@ -0,0 +1,57 @@
+
+import { loadMoonPay } from '@moonpay/moonpay-js';
+// import { MoonPayProvider, MoonPayBuyWidget } from '@moonpay/moonpay-react';
+import { ReactNode, useEffect, useState } from 'react';
+
+
+export default function MoonpayOnramp(props?: {
+ walletAddress?: string;
+ children?: ReactNode,
+}) {
+ // const [visible, setVisible] = useState(false);
+ const [moonPaySdk, setMoonPaySdk] = useState(undefined as any);
+ const element = props?.children || (Toggle widget );
+
+ useEffect(() => {
+ if (moonPaySdk) {
+ return;
+ }
+ loadMoonPay().then((moonPay) => {
+ if (!moonPay) {
+ return null;
+ }
+ const moonPaySdk = moonPay({
+ flow: 'buy',
+ environment: 'sandbox',
+ variant: 'overlay',
+ params: {
+ apiKey: '',
+ theme: 'dark',
+ baseCurrencyCode: 'usd',
+ baseCurrencyAmount: '100',
+ defaultCurrencyCode: 'eth'
+ }
+ });
+ setMoonPaySdk(moonPaySdk);
+ })
+ }, [moonPaySdk]);
+
+ return (
+ //
+ //
+ moonPaySdk?.show()}>
+ {element}
+
+ //
+
+ )
+}
\ No newline at end of file
diff --git a/src/containers/mobile/TokenDetailMobileContainer.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
index 50b87b66..5308d29a 100644
--- a/src/containers/mobile/TokenDetailMobileContainer.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -1,13 +1,36 @@
import { IAsset } from "@/interfaces/asset.interface";
import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
-import { IonAvatar, IonButton, IonCol, IonContent, IonFooter, IonGrid, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonNote, IonRow, IonText, IonToolbar } from "@ionic/react"
+import {
+ IonAvatar,
+ IonBadge,
+ IonButton,
+ IonChip,
+ IonCol,
+ IonContent,
+ IonFooter,
+ IonGrid,
+ IonHeader,
+ IonIcon,
+ IonItem,
+ IonLabel,
+ IonList,
+ IonListHeader,
+ IonNote,
+ IonRow,
+ IonText,
+ IonTitle,
+ IonToolbar,
+} from "@ionic/react";
import { MobileActionNavButtons } from "../../components/mobile/ActionNavButtons";
-import { Suspense, lazy, useEffect } from "react";
+import { Suspense, lazy, useEffect, useState } from "react";
import { ethers } from "ethers";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { CHAIN_AVAILABLES } from "@/constants/chains";
import { airplane, download, paperPlane } from "ionicons/icons";
+import { DataItem } from "@/components/ui/LightChart";
+import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
+import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
const LightChart = lazy(() => import("@/components/ui/LightChart"));
@@ -15,170 +38,475 @@ const getTxsFromAddress = async (address: string) => {
let provider = new ethers.providers.EtherscanProvider();
let history = await provider.getHistory(address);
console.log(history);
-}
-export const TokenDetailMobileContainer = (props: {
- data: {name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[]};
+};
+
+export const TokenDetailMobileContainer = (props: {
+ data: {
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+ };
dismiss: () => void;
setState: (state: any) => void;
setIsSwapModalOpen: (state: boolean) => void;
- })=> {
+}) => {
const { data, dismiss } = props;
const { walletAddress } = Store.useState(getWeb3State);
+ const [dataChartHistory, setDataChartHistory] = useState([]);
+ const [tokenInfo, setTokenInfo] = useState(undefined);
useEffect(() => {
if (!walletAddress) return;
getTxsFromAddress(walletAddress);
+ getTokenHistoryPrice(props.data.symbol).then((prices) => {
+ const data: DataItem[] = prices.map(([time, value]: string[]) => {
+ const dataItem = {
+ time: new Date(time).toISOString().split("T").shift() || "",
+ value: Number(value),
+ };
+ return dataItem;
+ });
+ setDataChartHistory(() => data.slice(0, data.length - 1));
+ });
+ getTokenInfo(props.data.symbol).then((tokenInfo) =>
+ setTokenInfo(() => tokenInfo)
+ );
}, [walletAddress]);
return (
<>
-
-
-
-
-
- {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${data.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
-
- {data.balance.toFixed(6)} {data.symbol}
-
-
-
-
- $ {data.balanceUsd.toFixed(2)}
-
-
-
-
+
+
+
+ {data.symbol}
+
+ $ {data.balanceUsd.toFixed(2)}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${data.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+
+ {data.balance.toFixed(6)} {data.symbol}
+
+
+
+
+ $ {data.balanceUsd.toFixed(2)}
+
+
+
+
+
+
+
+
- .....>} >
-
-
-
- 1 {data.symbol} = $ {data.priceUsd}
-
-
+ .....>}>
+
+
+
+ 1{' '}{data.symbol} = $ {data.priceUsd.toFixed(2)}
+
+
-
+
+
+
+ Networks
+
+
+ {data.assets
+ .sort((a, b) =>
+ a.chain && b.chain
+ ? a.chain.id - b.chain.id
+ : a.balance + b.balance
+ )
+ .map((token, index) => (
+
+
+ c.id === token.chain?.id
+ )?.logo
+ }
+ alt={token.symbol}
+ style={{ transform: "scale(1.01)" }}
+ onError={(event) => {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {token.chain?.name}
+
+
+ {token.balance.toFixed(6)} {token.symbol}
+
+
+ $ {token.balanceUsd.toFixed(2)}
+
+
+
+ ))}
+
+
+
+
+
+ {tokenInfo && (
+
+
-
- Networks
+
+ Market details
- {data.assets
- .sort((a, b) => a.chain && b.chain
- ? a.chain.id - b.chain.id
- : a.balance + b.balance
- )
- .map((token, index) =>
-
-
- c.id === token.chain?.id)?.logo}
- alt={token.symbol}
- style={{ transform: "scale(1.01)"}}
- onError={(event) => {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
+
+ Categories
+ {tokenInfo.categories.map((categorie) => (
+ {categorie}
+ ))}
+
+ {tokenInfo.market_data.market_cap.usd && (
+
+ Market Cap.
+
+ ${tokenInfo.market_data.market_cap.usd.toFixed(0)}
+
+
+ )}
+ { tokenInfo.market_data.fully_diluted_valuation.usd && (
+
+ Fully Diluted Valuation
+
+ $
+ {tokenInfo.market_data.fully_diluted_valuation.usd.toFixed(
+ 0
+ )}
+
+
+ )}
+ { tokenInfo.market_data.circulating_supply && (
+
+ Circulating supply
+
+ {tokenInfo.market_data.circulating_supply.toFixed(0)}
+
+
+ )}
+ { tokenInfo.market_data.total_supply && (
+
+ Total supply
+
+ {tokenInfo.market_data.total_supply.toFixed(0)}
+
+
+ )}
+ { tokenInfo.market_data.max_supply && (
+
+ Max supply
+
+ {tokenInfo.market_data.max_supply.toFixed(0)}
+
+
+ )}
+
- {token.chain?.name}
+ Historical Price
-
-
- {token.balance.toFixed(6)} {token.symbol}
+
+ { tokenInfo.market_data.current_price.usd && (
+
+ Current price
+
+ ${tokenInfo.market_data.current_price.usd}
+
+ {tokenInfo.market_data.price_change_percentage_24h_in_currency.usd.toFixed(
+ 2
+ )}
+ %
+
+
+
+
+ )}
+ { tokenInfo.market_data.ath.usd && (
+
+ All time height
+
+ ${tokenInfo.market_data.ath.usd}
+
+ {tokenInfo.market_data.ath_change_percentage.usd.toFixed(
+ 2
+ )}
+ %
+
+
- $ {token.balanceUsd.toFixed(2)}
+
+ {new Date(
+ tokenInfo.market_data.ath_date.usd
+ ).toLocaleDateString()}
+
-
-
-
+
+
+ )}
+ { tokenInfo.market_data.atl.usd&& (
+
+ All time low
+
+ ${tokenInfo.market_data.atl.usd}
+
+ {tokenInfo.market_data.atl_change_percentage.usd.toFixed(
+ 2
+ )}
+ %
+
+
+
+
+ {new Date(
+ tokenInfo.market_data.atl_date.usd
+ ).toLocaleDateString()}
+
+
+
+
)}
-
+
+
+
+ Datas are coming from Coingeeko API
+
+
+
+
+ )}
-
-
+
+
- {
+ {
props.setState({ isTransferModalOpen: true });
- }} >
-
- Send
+ }}
+ >
+
+ Send
- {
+ {
props.setState({ isDepositModalOpen: true });
- }} >
-
- Deposit
+ }}
+ >
+
+ Deposit
- {/* props.setState(state)}
setIsSwapModalOpen={() => props.setIsSwapModalOpen(true)}
hideEarnBtn={true} /> */}
@@ -186,4 +514,4 @@ export const TokenDetailMobileContainer = (props: {
>
);
-}
\ No newline at end of file
+};
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
index 22afad89..4021a85c 100644
--- a/src/containers/mobile/WalletMobileContainer.tsx
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -101,7 +101,7 @@ class WalletMobileContainer extends WalletBaseComponent<
>
-
+
@@ -128,7 +128,7 @@ class WalletMobileContainer extends WalletBaseComponent<
/>
{this.state.assetGroup.length > 0 && (
-
+
{
- if (
- this.props.walletAddress &&
- this.props.walletAddress !== "" &&
- this.props.isMagicWallet
- ) {
- const magic = await getMagic();
- magic.wallet.showOnRamp();
- } else {
- this.props.setIsAlertOpen(false);
- }
- }}
>
{
+ // convert symbol to coingeeko id
+ const responseList = localStorage.getItem('hexa-lite-coingeeko/coinList');
+ let data;
+ if (responseList) {
+ data = JSON.parse(responseList);
+ } else {
+ const fetchResponse = await fetch(`https://api.coingecko.com/api/v3/coins/list`);
+ data = await fetchResponse.json();
+ localStorage.setItem('hexa-lite-coingeeko/coinList', JSON.stringify(data));
+ }
+ const coin: {id?: string} = data.find((c: any) => c.symbol.toLowerCase() === symbol.toLowerCase());
+ return coin?.id;
+}
\ No newline at end of file
diff --git a/src/utils/getTokenHistoryPrice.ts b/src/utils/getTokenHistoryPrice.ts
new file mode 100644
index 00000000..63923370
--- /dev/null
+++ b/src/utils/getTokenHistoryPrice.ts
@@ -0,0 +1,33 @@
+
+export const getTokenHistoryPrice = async (symbol: string) => {
+ // convert symbol to coingeeko id
+ const responseList = localStorage.getItem('hexa-lite-coingeeko/coinList');
+ let data;
+ if (responseList) {
+ data = JSON.parse(responseList);
+ } else {
+ const fetchResponse = await fetch(`https://api.coingecko.com/api/v3/coins/list`);
+ data = await fetchResponse.json();
+ localStorage.setItem('hexa-lite-coingeeko/coinList', JSON.stringify(data));
+ }
+ const coin = data.find((c: any) => c.symbol.toLowerCase() === symbol.toLowerCase());
+ if (!coin) return [];
+
+ const responseToken = localStorage.getItem(`hexa-lite-coingeeko/coin/${coin.id}/market_chart`);
+ const jsonData = JSON.parse(responseToken||'{}');
+ const isDeadlineReach = (Date.now() - jsonData.timestamp) > (60 * 1000 * 30);
+ let tokenMarketData;
+ if (responseToken && !isDeadlineReach && jsonData.data) {
+ tokenMarketData = jsonData.data;
+ } else {
+ const url = `https://api.coingecko.com/api/v3/coins/${coin.id}/market_chart?vs_currency=usd&days=30&interval=daily`;
+ const res = await fetch(url);
+ const result = await res.json();
+ tokenMarketData = result?.prices as number[]||[];
+ localStorage.setItem(`hexa-lite-coingeeko/coin/${coin.id}/market_chart`, JSON.stringify({
+ data: tokenMarketData,
+ timestamp: Date.now()
+ }));
+ }
+ return tokenMarketData;
+}
\ No newline at end of file
diff --git a/src/utils/getTokenInfo.ts b/src/utils/getTokenInfo.ts
new file mode 100644
index 00000000..53d97ab2
--- /dev/null
+++ b/src/utils/getTokenInfo.ts
@@ -0,0 +1,71 @@
+import { getCoingeekoTokenId } from "./getCoingeekoTokenId";
+
+export type TokenInfo = {
+ description: {en: string};
+ categories: string[];
+ image: {
+ thumb: string;
+ small: string;
+ large: string;
+ };
+ market_data: {
+ ath: {usd: number};
+ ath_change_percentage: {usd: number};
+ ath_date: { usd: string };
+ atl: {usd: number};
+ atl_change_percentage: {usd: number};
+ atl_date: { usd: string };
+ circulating_supply: number;
+ current_price: { usd: number };
+ fully_diluted_valuation: { usd: number };
+ high_24h: { usd: number };
+ last_updated: string;
+ low_24h: { usd: number };
+ market_cap: { usd: number };
+ market_cap_change_24h: number;
+ market_cap_change_24h_in_currency: { usd: number };
+ market_cap_change_percentage_24h: number;
+ market_cap_change_percentage_24h_in_currency: { usd: number };
+ market_cap_fdv_ratio: number;
+ market_cap_rank: number;
+ max_supply: number;
+ price_change_24h: number;
+ price_change_24h_in_currency: { usd: number };
+ price_change_percentage_1h_in_currency: { usd: number };
+ price_change_percentage_1y_in_currency: { usd: number };
+ price_change_percentage_7d_in_currency: { usd: number };
+ price_change_percentage_14d_in_currency: { usd: number };
+ price_change_percentage_24h_in_currency: { usd: number };
+ price_change_percentage_30d_in_currency: { usd: number };
+ price_change_percentage_60d_in_currency: { usd: number };
+ price_change_percentage_200d_in_currency: { usd: number };
+ total_supply: number;
+ total_value_locked: number|null;
+ total_volume: { usd: number };
+ };
+ sentiment_votes_down_percentage: number;
+ sentiment_votes_up_percentage: number;
+
+};
+
+export const getTokenInfo = async (symbol: string) => {
+ const tokenId = await getCoingeekoTokenId(symbol);
+ if (!tokenId) return undefined;
+ // check localstorage if data is stored from less than 1 day
+ const response = localStorage.getItem(`hexa-lite-coingeeko/coin/${tokenId}/info`);
+ const jsonData = JSON.parse(response||'{}');
+ const isDeadlineReach = (Date.now() - jsonData.timestamp) > (60 * 1000 * 60 * 24);
+ let tokenInfo;
+ if (response && !isDeadlineReach && jsonData.data) {
+ tokenInfo = jsonData.data;
+ } else {
+ // fetch data from coingecko
+ tokenInfo = await fetch(`https://api.coingecko.com/api/v3/coins/${tokenId}?market_data=true&community_data=true`)
+ .then((res) => res.json());
+ localStorage.setItem(`hexa-lite-coingeeko/coin/${tokenId}/info`, JSON.stringify({
+ data: tokenInfo,
+ timestamp: Date.now()
+ }));
+ }
+ return tokenInfo as TokenInfo;
+}
\ No newline at end of file
From 2fd7894a86297358b0bca9d556ba7bffbe3cdac8 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Fri, 15 Mar 2024 16:50:27 +0100
Subject: [PATCH 10/74] refactor: add token market details
---
src/components/ui/LightChart.tsx | 2 +-
.../mobile/TokenDetailMobileContainer.tsx | 232 +++++++++++-------
src/styles/global.scss | 21 ++
src/utils/getCoingeekoTokenId.ts | 6 +-
4 files changed, 165 insertions(+), 96 deletions(-)
diff --git a/src/components/ui/LightChart.tsx b/src/components/ui/LightChart.tsx
index 58243481..644afdf6 100644
--- a/src/components/ui/LightChart.tsx
+++ b/src/components/ui/LightChart.tsx
@@ -23,7 +23,7 @@ export default function LightChart(props: { data: DataItem[] }) {
// create a new chart
const chart = createChart(chartContainerRef.current, {
- width: 400,
+ width: window.innerWidth||400,
height: 250,
layout: {
background: {
diff --git a/src/containers/mobile/TokenDetailMobileContainer.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
index 5308d29a..c90e1520 100644
--- a/src/containers/mobile/TokenDetailMobileContainer.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -1,6 +1,8 @@
import { IAsset } from "@/interfaces/asset.interface";
import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
import {
+ IonAccordion,
+ IonAccordionGroup,
IonAvatar,
IonBadge,
IonButton,
@@ -17,6 +19,8 @@ import {
IonListHeader,
IonNote,
IonRow,
+ IonSelect,
+ IonSelectOption,
IonText,
IonTitle,
IonToolbar,
@@ -27,7 +31,7 @@ import { ethers } from "ethers";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { CHAIN_AVAILABLES } from "@/constants/chains";
-import { airplane, download, paperPlane } from "ionicons/icons";
+import { airplane, chevronDown, download, paperPlane } from "ionicons/icons";
import { DataItem } from "@/components/ui/LightChart";
import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
@@ -101,19 +105,16 @@ export const TokenDetailMobileContainer = (props: {
-
+ >
-
+
$ {data.balanceUsd.toFixed(2)}
+ { tokenInfo?.market_data?.price_change_percentage_24h_in_currency?.usd && (
+
+
+ ({tokenInfo.market_data.price_change_percentage_24h_in_currency.usd.toFixed(
+ 2
+ )}
+ % /24h )
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ Networks details
+
+
+ {data.assets
+ .sort((a, b) =>
+ a.chain && b.chain
+ ? a.chain.id - b.chain.id
+ : a.balance + b.balance
+ )
+ .map((token, index) => (
+
+
+ c.id === token.chain?.id
+ )?.logo
+ }
+ alt={token.symbol}
+ style={{ transform: "scale(1.01)" }}
+ onError={(event) => {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {token.chain?.name}
+
+
+ {token.balance.toFixed(6)} {token.symbol}
+
+
+
+ $ {token.balanceUsd.toFixed(2)}
+
+
+
+
+ ))}
+
+
+
+
+
+
@@ -152,84 +247,25 @@ export const TokenDetailMobileContainer = (props: {
-
+
.....>}>
-
-
- 1{' '}{data.symbol} = $ {data.priceUsd.toFixed(2)}
-
+
+
+ 1 {data.symbol} = $ {(tokenInfo?.market_data?.current_price?.usd||data.priceUsd).toFixed(2)}
+
-
-
-
-
-
- Networks
-
-
- {data.assets
- .sort((a, b) =>
- a.chain && b.chain
- ? a.chain.id - b.chain.id
- : a.balance + b.balance
- )
- .map((token, index) => (
-
-
- c.id === token.chain?.id
- )?.logo
- }
- alt={token.symbol}
- style={{ transform: "scale(1.01)" }}
- onError={(event) => {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
- {token.chain?.name}
-
-
- {token.balance.toFixed(6)} {token.symbol}
-
-
- $ {token.balanceUsd.toFixed(2)}
-
-
-
- ))}
-
-
-
-
{tokenInfo && (
@@ -240,20 +276,22 @@ export const TokenDetailMobileContainer = (props: {
>
- Market details
+ Market details
-
Categories
- {tokenInfo.categories.map((categorie) => (
- {categorie}
- ))}
-
+
+ {tokenInfo.categories.map((categorie, i) => (
+ {categorie}
+ ))}
+
+ */}
{tokenInfo.market_data.market_cap.usd && (
)}
- { tokenInfo.market_data.fully_diluted_valuation.usd && (
+ {tokenInfo.market_data.fully_diluted_valuation.usd && (
- Fully Diluted Valuation
+
+ Fully Diluted Valuation
+
)}
- { tokenInfo.market_data.circulating_supply && (
+ {tokenInfo.market_data.circulating_supply && (
)}
- { tokenInfo.market_data.total_supply && (
+ {tokenInfo.market_data.total_supply && (
)}
- { tokenInfo.market_data.max_supply && (
+ {tokenInfo.market_data.max_supply && (
- Historical Price
+ Historical Price
- { tokenInfo.market_data.current_price.usd && (
+ {tokenInfo.market_data.current_price.usd && (
-
+
+ (24h change)
+
)}
- { tokenInfo.market_data.ath.usd && (
+ {tokenInfo.market_data.ath.usd && (
)}
- { tokenInfo.market_data.atl.usd&& (
+ {tokenInfo.market_data.atl.usd && (
- Datas are coming from Coingeeko API
+ Market datas from Coingeeko API
+ Last update: {new Date(tokenInfo?.market_data?.last_updated||new Date ().toLocaleDateString()).toLocaleString()}
+
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 951f05b9..c3b7d0b4 100755
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -630,6 +630,27 @@ ion-accordion:not(.faq) {
}
}
+ion-accordion.networkList {
+ &:not(.accordion-collapsed):not(.accordion-collapsing) {
+ background: transparent!important;
+ }
+ &:hover,
+ &.accordion-collapsed:hover {
+ background: transparent!important;
+ }
+
+ *[slot=header] {
+ cursor: pointer;
+ }
+
+ *[slot=content] ion-item {
+ --background: transparent!important;
+ }
+ // &.accordion-expanded ion-item[slot='header'] {
+ // --color: red;
+ // }
+}
+
.verticalLineBefore::before {
content: '';
position: absolute;
diff --git a/src/utils/getCoingeekoTokenId.ts b/src/utils/getCoingeekoTokenId.ts
index 515ad183..e5006fa8 100644
--- a/src/utils/getCoingeekoTokenId.ts
+++ b/src/utils/getCoingeekoTokenId.ts
@@ -10,6 +10,10 @@ export const getCoingeekoTokenId = async (symbol: string) => {
data = await fetchResponse.json();
localStorage.setItem('hexa-lite-coingeeko/coinList', JSON.stringify(data));
}
- const coin: {id?: string} = data.find((c: any) => c.symbol.toLowerCase() === symbol.toLowerCase());
+ const coin: {id?: string} = data.find(
+ (c: any) =>
+ c.symbol.toLowerCase() === symbol.toLowerCase()
+ && !c.name.toLowerCase().includes('bridged')
+ );
return coin?.id;
}
\ No newline at end of file
From dc82fe31f1d144fff077b443d0a463d84c3b1dc2 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Sat, 16 Mar 2024 00:28:04 +0100
Subject: [PATCH 11/74] refactor: improve wallet token detail
---
src/components/ReserveDetail.tsx | 6 +-
src/components/Welcome.tsx | 4 +-
src/components/base/WalletBaseContainer.tsx | 24 +-
src/components/ui/LightChart.tsx | 4 +-
src/components/ui/WalletAssetEntity.tsx | 98 ++++
src/containers/desktop/DefiContainer.tsx | 22 +-
.../desktop/TokenDetailDesktopContainer.tsx | 553 ++++++++++++++++++
.../desktop/WalletDesktopContainer.tsx | 196 +++----
src/utils/currency-format.ts | 12 -
src/utils/currencyFormat.ts | 14 +
src/utils/numberFormat.ts | 6 +
11 files changed, 778 insertions(+), 161 deletions(-)
create mode 100644 src/components/ui/WalletAssetEntity.tsx
create mode 100644 src/containers/desktop/TokenDetailDesktopContainer.tsx
delete mode 100644 src/utils/currency-format.ts
create mode 100644 src/utils/currencyFormat.ts
create mode 100644 src/utils/numberFormat.ts
diff --git a/src/components/ReserveDetail.tsx b/src/components/ReserveDetail.tsx
index e2e79de5..ad31c732 100644
--- a/src/components/ReserveDetail.tsx
+++ b/src/components/ReserveDetail.tsx
@@ -58,7 +58,6 @@ import {
import { useLoader } from "../context/LoaderContext";
import { getAssetIconUrl } from "../utils/getAssetIconUrl";
import { SymbolIcon } from "./SymbolIcon";
-import { currencyFormat } from "../utils/currency-format";
import { ApyDetail } from "./ApyDetail";
import { AavePool, IAavePool } from "@/pool/Aave.pool";
import { MarketPool } from "@/pool/Market.pool";
@@ -75,6 +74,7 @@ import {
} from "@/utils/getPoolWalletBalance";
import { initializeUserSummary } from "@/store/effects/pools.effect";
import { ModalMessage } from "./ModalMessage";
+import { currencyFormat } from "@/utils/currencyFormat";
interface IReserveDetailProps {
pool: MarketPool;
@@ -818,14 +818,14 @@ export function ReserveDetail(props: IReserveDetailProps) {
) > 0 && (
<>
- {currencyFormat(
+ {currencyFormat.format(
Number(
protocolSummary?.totalBorrowsUSD ||
0
)
)}{" "}
of{" "}
- {currencyFormat(
+ {currencyFormat.format(
Number(
protocolSummary?.totalCollateralUSD ||
0
diff --git a/src/components/Welcome.tsx b/src/components/Welcome.tsx
index 91653165..35047792 100644
--- a/src/components/Welcome.tsx
+++ b/src/components/Welcome.tsx
@@ -61,7 +61,7 @@ export function Welcome({
style={{marginTop: '2rem'}}
onClick={(e) => {
router.push("swap");
- handleSegmentChange({ detail: { value: "swap" } });
+ handleSegmentChange({ detail: { value: "wallet" } });
}}
>
Launch App
@@ -672,7 +672,7 @@ export function Welcome({
color="gradient"
onClick={(e) =>{
router.push("swap");
- handleSegmentChange({ detail: { value: "swap" } })
+ handleSegmentChange({ detail: { value: "wallet" } })
}}
>
Launch App
diff --git a/src/components/base/WalletBaseContainer.tsx b/src/components/base/WalletBaseContainer.tsx
index 4dade31e..b705c203 100644
--- a/src/components/base/WalletBaseContainer.tsx
+++ b/src/components/base/WalletBaseContainer.tsx
@@ -5,6 +5,16 @@ import { DepositContainer } from "@/containers/DepositContainer";
import { HookOverlayOptions } from "@ionic/react/dist/types/hooks/HookOverlayOptions";
import { TransferContainer } from "../../containers/TransferContainer";
+export type SelectedTokenDetail = {
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+};
+
export interface WalletComponentProps {
modalOpts: Omit &
HookOverlayOptions;
@@ -16,24 +26,12 @@ export interface WalletComponentState {
filterBy: string | null;
assetGroup: any[];
totalBalance: number;
- selectedTokenDetail: {
- name: string;
- symbol: string;
- priceUsd: number;
- balance: number;
- balanceUsd: number;
- thumbnail: string;
- assets: IAsset[];
- } | null;
+ selectedTokenDetail: SelectedTokenDetail | null;
isEarnModalOpen: boolean;
isTransferModalOpen: boolean;
isDepositModalOpen: boolean;
}
-interface demo {
- demo: string;
-}
-
export default class WalletBaseComponent extends React.Component<
T & WalletComponentProps,
WalletComponentState
diff --git a/src/components/ui/LightChart.tsx b/src/components/ui/LightChart.tsx
index 644afdf6..209f9311 100644
--- a/src/components/ui/LightChart.tsx
+++ b/src/components/ui/LightChart.tsx
@@ -8,7 +8,7 @@ export interface DataItem {
value: number;
}
-export default function LightChart(props: { data: DataItem[] }) {
+export default function LightChart(props: { data: DataItem[], minHeight?: number }) {
const chartContainerRef = useRef(null);
const chartRef = useRef(null);
@@ -24,7 +24,7 @@ export default function LightChart(props: { data: DataItem[] }) {
// create a new chart
const chart = createChart(chartContainerRef.current, {
width: window.innerWidth||400,
- height: 250,
+ height: props?.minHeight || 250,
layout: {
background: {
type: ColorType.Solid,
diff --git a/src/components/ui/WalletAssetEntity.tsx b/src/components/ui/WalletAssetEntity.tsx
new file mode 100644
index 00000000..905d9666
--- /dev/null
+++ b/src/components/ui/WalletAssetEntity.tsx
@@ -0,0 +1,98 @@
+import { IAsset } from "@/interfaces/asset.interface";
+import { currencyFormat } from "@/utils/currencyFormat";
+import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
+import { numberFormat } from "@/utils/numberFormat";
+import {
+ IonAvatar,
+ IonCol,
+ IonGrid,
+ IonLabel,
+ IonRow,
+ IonText,
+} from "@ionic/react";
+import { SelectedTokenDetail } from "../base/WalletBaseContainer";
+
+export function WalletAssetEntity(props: {
+ asset: SelectedTokenDetail;
+ setSelectedTokenDetail: (asset: SelectedTokenDetail) => void;
+}) {
+ const { asset, setSelectedTokenDetail } = props;
+
+ return (
+ {
+ setSelectedTokenDetail(asset);
+ }}
+ style={{
+ cursor: "pointer",
+ borderBottom: "solid 1px rgba(var(--ion-color-primary-rgb), 0.2)",
+ }}
+ >
+
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {asset.symbol}
+
+ {asset.name}
+
+
+
+
+
+
+
+
+ {currencyFormat.format(asset.priceUsd)}
+
+
+
+
+ {numberFormat.format(asset.balance)}
+
+
+
+
+
+ {currencyFormat.format(asset.balanceUsd)}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/desktop/DefiContainer.tsx b/src/containers/desktop/DefiContainer.tsx
index 7e5f7f87..d2e008d8 100644
--- a/src/containers/desktop/DefiContainer.tsx
+++ b/src/containers/desktop/DefiContainer.tsx
@@ -21,13 +21,13 @@ import { getPercent } from "../../utils/utils";
import { CHAIN_AVAILABLES } from "../../constants/chains";
import { useEffect, useState } from "react";
import { MarketList } from "../../components/MarketsList";
-import { currencyFormat } from "../../utils/currency-format";
import { valueToBigNumber } from "@aave/math-utils";
import { getReadableValue } from "@/utils/getReadableValue";
import Store from "@/store";
import { getPoolGroupsState, getProtocolSummaryState, getUserSummaryAndIncentivesGroupState, getWeb3State } from "@/store/selectors";
import { initializePools, initializeUserSummary } from "@/store/effects/pools.effect";
import { patchPoolsState } from "@/store/actions";
+import { currencyFormat } from "@/utils/currencyFormat";
export const minBaseTokenRemainingByNetwork: Record = {
[ChainId.optimism]: "0.0001",
@@ -183,7 +183,7 @@ export default function DefiContainer({
size-md="4"
class=" ion-padding-vertical"
>
- {currencyFormat(totalSupplyUsd)}
+ {currencyFormat.format(totalSupplyUsd)}
DEPOSIT BALANCE
@@ -205,8 +205,8 @@ export default function DefiContainer({
- {currencyFormat(+totalBorrowsUsd)} of{" "}
- {currencyFormat(totalBorrowableUsd)}
+ {currencyFormat.format(+totalBorrowsUsd)} of{" "}
+ {currencyFormat.format(totalBorrowableUsd)}
@@ -216,7 +216,7 @@ export default function DefiContainer({
size-md="4"
class=" ion-padding-vertical"
>
- {currencyFormat(totalAbailableToBorrow)}
+ {currencyFormat.format(totalAbailableToBorrow)}
AVAILABLE TO BORROW
@@ -374,11 +374,11 @@ export default function DefiContainer({
class="ion-padding-horizontal ion-text-end ion-hide-md-down"
>
- {currencyFormat(+summary.totalSupplyUSD)}
+ {currencyFormat.format(+summary.totalSupplyUSD)}
- {currencyFormat(
+ {currencyFormat.format(
summary.totalSupplyUSD *
summary.currentLiquidationThreshold
)}{" "}
@@ -393,7 +393,7 @@ export default function DefiContainer({
class="ion-padding-horizontal ion-text-end ion-hide-md-down"
>
- {currencyFormat(+summary.totalBorrowsUSD)}
+ {currencyFormat.format(+summary.totalBorrowsUSD)}
- {currencyFormat(
+ {currencyFormat.format(
(summary.totalCollateralUSD *
summary.currentLiquidationThreshold) -
summary.totalBorrowsUSD
@@ -415,8 +415,8 @@ export default function DefiContainer({
class="ion-padding-horizontal ion-text-end"
>
- {currencyFormat(+summary.totalBorrowsUSD)} of{" "}
- {currencyFormat(
+ {currencyFormat.format(+summary.totalBorrowsUSD)} of{" "}
+ {currencyFormat.format(
summary.totalCollateralUSD *
summary.currentLiquidationThreshold
)}{" "}
diff --git a/src/containers/desktop/TokenDetailDesktopContainer.tsx b/src/containers/desktop/TokenDetailDesktopContainer.tsx
new file mode 100644
index 00000000..2f0ce455
--- /dev/null
+++ b/src/containers/desktop/TokenDetailDesktopContainer.tsx
@@ -0,0 +1,553 @@
+import { IAsset } from "@/interfaces/asset.interface";
+import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
+import {
+ IonAccordion,
+ IonAccordionGroup,
+ IonAvatar,
+ IonBadge,
+ IonButton,
+ IonChip,
+ IonCol,
+ IonContent,
+ IonFooter,
+ IonGrid,
+ IonHeader,
+ IonIcon,
+ IonItem,
+ IonLabel,
+ IonList,
+ IonListHeader,
+ IonNote,
+ IonRow,
+ IonSelect,
+ IonSelectOption,
+ IonSpinner,
+ IonText,
+ IonTitle,
+ IonToolbar,
+} from "@ionic/react";
+import { MobileActionNavButtons } from "../../components/mobile/ActionNavButtons";
+import { Suspense, lazy, useEffect, useState } from "react";
+import { ethers } from "ethers";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import { CHAIN_AVAILABLES } from "@/constants/chains";
+import { airplane, chevronDown, close, download, paperPlane, repeat } from "ionicons/icons";
+import { DataItem } from "@/components/ui/LightChart";
+import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
+import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
+import { numberFormat } from "@/utils/numberFormat";
+import { currencyFormat } from "@/utils/currencyFormat";
+
+const LightChart = lazy(() => import("@/components/ui/LightChart"));
+
+const getTxsFromAddress = async (address: string) => {
+ let provider = new ethers.providers.EtherscanProvider();
+ let history = await provider.getHistory(address);
+ console.log(history);
+};
+
+export const TokenDetailDesktopContainer = (props: {
+ data: {
+ name: string;
+ symbol: string;
+ priceUsd: number;
+ balance: number;
+ balanceUsd: number;
+ thumbnail: string;
+ assets: IAsset[];
+ };
+ dismiss: () => void;
+ setState: (state: any) => void;
+}) => {
+ const { data, dismiss } = props;
+ const { walletAddress } = Store.useState(getWeb3State);
+ const [dataChartHistory, setDataChartHistory] = useState([]);
+ const [tokenInfo, setTokenInfo] = useState(undefined);
+
+ useEffect(() => {
+ if (!walletAddress) return;
+ getTxsFromAddress(walletAddress);
+ getTokenHistoryPrice(props.data.symbol).then((prices) => {
+ const data: DataItem[] = prices.map(([time, value]: string[]) => {
+ const dataItem = {
+ time: new Date(time).toISOString().split("T").shift() || "",
+ value: Number(value),
+ };
+ return dataItem;
+ });
+ setDataChartHistory(() => data.slice(0, data.length - 1));
+ });
+ getTokenInfo(props.data.symbol).then((tokenInfo) =>
+ setTokenInfo(() => tokenInfo)
+ );
+ }, [walletAddress]);
+
+ return (
+ <>
+
+
+
+ {data.symbol}
+
+ $ {data.balanceUsd.toFixed(2)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${data.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+
+ {data.balance.toFixed(6)} {data.symbol}
+
+
+
+
+ $ {data.balanceUsd.toFixed(2)}
+ { tokenInfo?.market_data?.price_change_percentage_24h_in_currency?.usd && (
+
+
+ ({tokenInfo.market_data.price_change_percentage_24h_in_currency.usd.toFixed(
+ 2
+ )}
+ % /24h )
+
+
+ )}
+
+
+
+
+
+ {
+ props.setState({ isTransferModalOpen: true });
+ }}>
+
+ Send
+
+ {
+ props.setState({ isDepositModalOpen: true });
+ }}>
+
+ Deposit
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Networks details
+
+
+ {data.assets
+ .sort((a, b) =>
+ a.chain && b.chain
+ ? a.chain.id - b.chain.id
+ : a.balance + b.balance
+ )
+ .map((token, index) => (
+
+
+ c.id === token.chain?.id
+ )?.logo
+ }
+ alt={token.symbol}
+ style={{ transform: "scale(1.01)" }}
+ onError={(event) => {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+ {token.chain?.name}
+
+
+ { numberFormat.format(token.balance)} {token.symbol}
+
+
+
+ {currencyFormat.format(token.balanceUsd)}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >}>
+
+
+ {props.data.symbol} / USD
+
+ 1 {data.symbol} = {currencyFormat.format(tokenInfo?.market_data?.current_price?.usd||data.priceUsd)}
+
+
+
+
+
+
+
+
+ {tokenInfo && (
+
+
+
+
+
+ Market details
+
+
+ {Boolean(tokenInfo.market_data.market_cap.usd) && (
+
+ Market Cap.
+
+ {currencyFormat.format(tokenInfo.market_data.market_cap.usd)}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.fully_diluted_valuation.usd) && (
+
+
+ Fully Diluted Valuation
+
+
+ {currencyFormat.format(tokenInfo.market_data.fully_diluted_valuation.usd)}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.circulating_supply) && (
+
+ Circulating supply
+
+ {numberFormat.format(tokenInfo.market_data.circulating_supply)}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.total_supply) && (
+
+ Total supply
+
+ {numberFormat.format(tokenInfo.market_data.total_supply)}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.max_supply) && (
+
+ Max supply
+
+ {numberFormat.format(tokenInfo.market_data.max_supply)}
+
+
+ )}
+
+
+
+
+ Historical Price
+
+
+ {Boolean(tokenInfo.market_data.current_price.usd) && (
+
+ Current price
+
+ {currencyFormat.format(tokenInfo.market_data.current_price.usd)}
+
+ {numberFormat.format(tokenInfo.market_data.price_change_percentage_24h_in_currency.usd)}
+ %
+
+
+ (24h change)
+
+
+
+ )}
+ {Boolean(tokenInfo.market_data.ath.usd) && (
+
+ All time height
+
+ {currencyFormat.format(tokenInfo.market_data.ath.usd)}
+
+ {numberFormat.format(tokenInfo.market_data.ath_change_percentage.usd)}%
+
+
+
+
+ {new Date(
+ tokenInfo.market_data.ath_date.usd
+ ).toLocaleDateString()}
+
+
+
+
+ )}
+ {Boolean(tokenInfo.market_data.atl.usd) && (
+
+ All time low
+
+ {currencyFormat.format(tokenInfo.market_data.atl.usd)}
+
+ {numberFormat.format(tokenInfo.market_data.atl_change_percentage.usd)}%
+
+
+
+
+ {new Date(
+ tokenInfo.market_data.atl_date.usd
+ ).toLocaleDateString()}
+
+
+
+
+ )}
+
+
+
+
+
+ Description
+
+
+
+
+ {tokenInfo.description.en}
+
+
+
+
+
+ Categories
+
+
+
+ {tokenInfo.categories.map((categorie, i) => (
+ {categorie}
+ ))}
+
+
+
+
+ )}
+
+
+
+
+
+ Market datas from Coingeeko API
+ Last update: {new Date(tokenInfo?.market_data?.last_updated||new Date ().toLocaleDateString()).toLocaleString()}
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
index 01cb08ec..6499d2e7 100644
--- a/src/containers/desktop/WalletDesktopContainer.tsx
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -3,7 +3,6 @@ import WalletBaseComponent, {
WalletComponentProps,
} from "../../components/base/WalletBaseContainer";
import {
- IonAvatar,
IonButton,
IonCard,
IonCardContent,
@@ -11,17 +10,17 @@ import {
IonGrid,
IonIcon,
IonLabel,
+ IonModal,
IonRow,
IonSearchbar,
- IonSelect,
- IonSelectOption,
IonText,
} from "@ionic/react";
import ConnectButton from "@/components/ConnectButton";
-import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
-import { CHAIN_AVAILABLES } from "@/constants/chains";
+import { TokenDetailDesktopContainer } from "./TokenDetailDesktopContainer";
+import { currencyFormat } from "@/utils/currencyFormat";
+import { WalletAssetEntity } from "@/components/ui/WalletAssetEntity";
class WalletDesktopContainer extends WalletBaseComponent {
constructor(props: WalletComponentProps) {
@@ -50,7 +49,7 @@ class WalletDesktopContainer extends WalletBaseComponent {
- $ {this.state.totalBalance.toFixed(2)}
+ {currencyFormat.format(this.state.totalBalance)}
@@ -197,11 +196,12 @@ class WalletDesktopContainer extends WalletBaseComponent {
<>
- {
- this.handleSearchChange(e);
- }} />
+ {
+ this.handleSearchChange(e);
+ }}
+ />
@@ -215,129 +215,89 @@ class WalletDesktopContainer extends WalletBaseComponent {
>
-
- Asset
+
+
+ Asset
+
-
- Price
-
-
- Balance
-
-
- Value
-
-
-
-
- {this.state.assetGroup
- .filter((asset) =>
- this.state.filterBy
- ? asset.symbol
- .toLowerCase()
- .includes(this.state.filterBy.toLowerCase())
- : true
- )
- .map((asset, index) => {
- return (
- {
- console.log("asset", asset);
- }}
- style={{
- borderBottom:
- "solid 1px rgba(var(--ion-color-primary-rgb), 0.2)",
- }}
- >
-
-
-
+
+
- {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
- {asset.symbol}
- {asset.name}
-
-
-
-
-
-
- $ {asset.priceUsd.toFixed(2)}
+ Price
-
-
-
- {asset.balance.toFixed(2)}
+
+
+
+ Balance
-
-
-
- $ {asset.balanceUsd.toFixed(2)}
+
+
+
+ Value
-
-
-
-
- );
- })}
+
+
+
+
+
+
+ {this.state.assetGroup
+ .filter((asset) =>
+ this.state.filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(this.state.filterBy.toLowerCase())
+ : true
+ )
+ .map((asset, index) => {
+ return (
+
+ this.handleTokenDetailClick(asset)
+ }
+ asset={asset}
+ key={index}
+ />
+ );
+ })}
>
)}
+
+ this.handleTokenDetailClick(null)}
+ className="modalPage"
+ >
+ {this.state.selectedTokenDetail && (
+ this.setState(state)}
+ data={this.state.selectedTokenDetail}
+ dismiss={() => this.handleTokenDetailClick(null)}
+ />
+ )}
+
>
);
}
diff --git a/src/utils/currency-format.ts b/src/utils/currency-format.ts
deleted file mode 100644
index 37cdb071..00000000
--- a/src/utils/currency-format.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-
-export const currencyFormat = (
- num: number,
- ops?: { currency?: string; language?: string }
-) => {
- const currency = ops?.currency || "USD";
- const language = ops?.language || "en-US";
- return num.toLocaleString(language, {
- style: "currency",
- currency,
- });
-}
\ No newline at end of file
diff --git a/src/utils/currencyFormat.ts b/src/utils/currencyFormat.ts
new file mode 100644
index 00000000..6bfe3278
--- /dev/null
+++ b/src/utils/currencyFormat.ts
@@ -0,0 +1,14 @@
+
+// format number to US dollar
+export const USDollar = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+});
+
+export const currencyFormat = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ compactDisplay: 'short',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+});
\ No newline at end of file
diff --git a/src/utils/numberFormat.ts b/src/utils/numberFormat.ts
new file mode 100644
index 00000000..b7c132b5
--- /dev/null
+++ b/src/utils/numberFormat.ts
@@ -0,0 +1,6 @@
+
+export const numberFormat = new Intl.NumberFormat('en-US', {
+ style: 'decimal',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+});
\ No newline at end of file
From 32bf868a6173fe3a3ca220e149de1be6aa639b09 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Sat, 16 Mar 2024 02:04:04 +0100
Subject: [PATCH 12/74] refactor: improve ui and add Haptics
---
package-lock.json | 9 +
package.json | 1 +
src/components/base/WalletBaseContainer.tsx | 10 +-
src/components/ui/MenuSetting.tsx | 74 +++++
src/components/ui/TokenDetailDescription.tsx | 31 +++
src/components/ui/TokenDetailMarketData.tsx | 243 ++++++++++++++++
.../desktop/TokenDetailDesktopContainer.tsx | 236 +---------------
.../mobile/TokenDetailMobileContainer.tsx | 260 ++----------------
.../mobile/WalletMobileContainer.tsx | 89 ++++--
9 files changed, 460 insertions(+), 493 deletions(-)
create mode 100644 src/components/ui/MenuSetting.tsx
create mode 100644 src/components/ui/TokenDetailDescription.tsx
create mode 100644 src/components/ui/TokenDetailMarketData.tsx
diff --git a/package-lock.json b/package-lock.json
index 64467e18..bb228caf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@capacitor/android": "^5.0.0",
"@capacitor/core": "^5.0.0",
"@capacitor/dialog": "^5.0.7",
+ "@capacitor/haptics": "^5.0.7",
"@capacitor/ios": "^5.0.0",
"@capacitor/status-bar": "^5.0.0",
"@cosmjs/stargate": "^0.32.1",
@@ -1515,6 +1516,14 @@
"@capacitor/core": "^5.0.0"
}
},
+ "node_modules/@capacitor/haptics": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-5.0.7.tgz",
+ "integrity": "sha512-/j+7Qa4BxQA5aOU43cwXuiudfSXfoHFsAVfcehH5DkSjxLykZKWHEuE4uFJXqdkSIbAHjS37D0Sde6ENP6G/MA==",
+ "peerDependencies": {
+ "@capacitor/core": "^5.0.0"
+ }
+ },
"node_modules/@capacitor/ios": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-5.6.0.tgz",
diff --git a/package.json b/package.json
index f70c4e99..895b95c3 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@capacitor/android": "^5.0.0",
"@capacitor/core": "^5.0.0",
"@capacitor/dialog": "^5.0.7",
+ "@capacitor/haptics": "^5.0.7",
"@capacitor/ios": "^5.0.0",
"@capacitor/status-bar": "^5.0.0",
"@cosmjs/stargate": "^0.32.1",
diff --git a/src/components/base/WalletBaseContainer.tsx b/src/components/base/WalletBaseContainer.tsx
index b705c203..e01f0c76 100644
--- a/src/components/base/WalletBaseContainer.tsx
+++ b/src/components/base/WalletBaseContainer.tsx
@@ -113,11 +113,11 @@ export default class WalletBaseComponent extends React.Component<
this.setState({ assetGroup });
}
- handleSearchChange(e: CustomEvent) {
+ async handleSearchChange(e: CustomEvent) {
this.setState({ filterBy: e.detail.value });
}
- handleTokenDetailClick(token: any = null) {
+ async handleTokenDetailClick(token: any = null) {
console.log(token);
this.setState((prev) =>({
...prev,
@@ -125,16 +125,16 @@ export default class WalletBaseComponent extends React.Component<
}));
}
- handleEarnClick() {
+ async handleEarnClick() {
this.setState({ isEarnModalOpen: !this.state.isEarnModalOpen });
}
- handleTransferClick(state: boolean) {
+ async handleTransferClick(state: boolean) {
console.log('handleTransferClick', state)
this.setState({isTransferModalOpen: state});
}
- handleDepositClick(state?: boolean) {
+ async handleDepositClick(state?: boolean) {
this.setState({
isDepositModalOpen:
state !== undefined ? state : !this.state.isDepositModalOpen,
diff --git a/src/components/ui/MenuSetting.tsx b/src/components/ui/MenuSetting.tsx
new file mode 100644
index 00000000..3b7987a3
--- /dev/null
+++ b/src/components/ui/MenuSetting.tsx
@@ -0,0 +1,74 @@
+import React, { useRef, useState } from "react";
+import {
+ IonMenu,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+ IonItem,
+ IonIcon,
+ IonLabel,
+ IonText,
+ IonItemDivider,
+ IonButton,
+ IonModal,
+ IonFooter,
+} from "@ionic/react";
+import { radioButtonOn, ribbonOutline } from "ionicons/icons";
+import { getAddressPoints } from "@/servcies/datas.service";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import ConnectButton from "../ConnectButton";
+import DisconnectButton from "../DisconnectButton";
+
+interface MenuSettingsProps {}
+
+export const MenuSettings: React.FC = ({}) => {
+ const menuRef = useRef(null);
+ const { walletAddress } = Store.useState(getWeb3State);
+ const [points, setPoints] = useState(null);
+
+ return (
+ <>
+
+
+ Settings
+
+
+
+ {
+ menuRef.current?.close();
+ }}
+ >
+
+
+ Wallet
+
+
+
+ {walletAddress}
+
+
+
+
+
+
+ {`HexaLite v${process.env.NEXT_PUBLIC_APP_VERSION} - ${process.env.NEXT_PUBLIC_APP_BUILD_DATE}`}
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/ui/TokenDetailDescription.tsx b/src/components/ui/TokenDetailDescription.tsx
new file mode 100644
index 00000000..ed677f49
--- /dev/null
+++ b/src/components/ui/TokenDetailDescription.tsx
@@ -0,0 +1,31 @@
+import { TokenInfo } from "@/utils/getTokenInfo";
+import { IonChip, IonLabel, IonListHeader, IonText } from "@ionic/react";
+
+export function TokenDetailDescription(props: { tokenInfo: TokenInfo }) {
+ const { tokenInfo } = props;
+
+ return (
+ <>
+
+
+ Description
+
+
+
+ {tokenInfo.description.en}
+
+
+
+
+ Categories
+
+
+
+ {tokenInfo.categories.map((categorie, i) => (
+ {categorie}
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/components/ui/TokenDetailMarketData.tsx b/src/components/ui/TokenDetailMarketData.tsx
new file mode 100644
index 00000000..dc7cecd0
--- /dev/null
+++ b/src/components/ui/TokenDetailMarketData.tsx
@@ -0,0 +1,243 @@
+import { currencyFormat } from "@/utils/currencyFormat";
+import { TokenInfo } from "@/utils/getTokenInfo";
+import { numberFormat } from "@/utils/numberFormat";
+import {
+ IonItem,
+ IonLabel,
+ IonList,
+ IonListHeader,
+ IonNote,
+ IonText,
+} from "@ionic/react";
+
+export function TokenDetailMarketDetail(props: {
+ tokenInfo: TokenInfo
+}) {
+ const { tokenInfo } = props;
+ return (
+ <>
+
+
+
+ Market details
+
+
+ {Boolean(tokenInfo.market_data.market_cap.usd) && (
+
+ Market Cap.
+
+ {currencyFormat.format(tokenInfo.market_data.market_cap.usd)}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.fully_diluted_valuation.usd) && (
+
+ Fully Diluted Valuation
+
+ {currencyFormat.format(
+ tokenInfo.market_data.fully_diluted_valuation.usd
+ )}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.circulating_supply) && (
+
+ Circulating supply
+
+ {numberFormat.format(tokenInfo.market_data.circulating_supply)}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.total_supply) && (
+
+ Total supply
+
+ {numberFormat.format(tokenInfo.market_data.total_supply)}
+
+
+ )}
+ {Boolean(tokenInfo.market_data.max_supply) && (
+
+ Max supply
+
+ {numberFormat.format(tokenInfo.market_data.max_supply)}
+
+
+ )}
+
+
+
+
+ Historical Price
+
+
+ {Boolean(tokenInfo.market_data.current_price.usd) && (
+
+ Current price
+
+ {currencyFormat.format(tokenInfo.market_data.current_price.usd)}
+
+ {numberFormat.format(
+ tokenInfo.market_data.price_change_percentage_24h_in_currency
+ .usd
+ )}
+ %
+
+
+
+ (24h change)
+
+
+
+
+ )}
+ {Boolean(tokenInfo.market_data.ath.usd) && (
+
+ All time height
+
+ {currencyFormat.format(tokenInfo.market_data.ath.usd)}
+
+ {numberFormat.format(
+ tokenInfo.market_data.ath_change_percentage.usd
+ )}
+ %
+
+
+
+
+ {new Date(
+ tokenInfo.market_data.ath_date.usd
+ ).toLocaleDateString()}
+
+
+
+
+ )}
+ {Boolean(tokenInfo.market_data.atl.usd) && (
+
+ All time low
+
+ {currencyFormat.format(tokenInfo.market_data.atl.usd)}
+
+ {numberFormat.format(
+ tokenInfo.market_data.atl_change_percentage.usd
+ )}
+ %
+
+
+
+
+ {new Date(
+ tokenInfo.market_data.atl_date.usd
+ ).toLocaleDateString()}
+
+
+
+
+ )}
+
+ >
+ );
+}
diff --git a/src/containers/desktop/TokenDetailDesktopContainer.tsx b/src/containers/desktop/TokenDetailDesktopContainer.tsx
index 2f0ce455..0bfbc539 100644
--- a/src/containers/desktop/TokenDetailDesktopContainer.tsx
+++ b/src/containers/desktop/TokenDetailDesktopContainer.tsx
@@ -38,6 +38,8 @@ import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
import { numberFormat } from "@/utils/numberFormat";
import { currencyFormat } from "@/utils/currencyFormat";
+import { TokenDetailDescription } from "@/components/ui/TokenDetailDescription";
+import { TokenDetailMarketDetail } from "@/components/ui/TokenDetailMarketData";
const LightChart = lazy(() => import("@/components/ui/LightChart"));
@@ -297,239 +299,10 @@ export const TokenDetailDesktopContainer = (props: {
{tokenInfo && (
-
-
-
- Market details
-
-
- {Boolean(tokenInfo.market_data.market_cap.usd) && (
-
- Market Cap.
-
- {currencyFormat.format(tokenInfo.market_data.market_cap.usd)}
-
-
- )}
- {Boolean(tokenInfo.market_data.fully_diluted_valuation.usd) && (
-
-
- Fully Diluted Valuation
-
-
- {currencyFormat.format(tokenInfo.market_data.fully_diluted_valuation.usd)}
-
-
- )}
- {Boolean(tokenInfo.market_data.circulating_supply) && (
-
- Circulating supply
-
- {numberFormat.format(tokenInfo.market_data.circulating_supply)}
-
-
- )}
- {Boolean(tokenInfo.market_data.total_supply) && (
-
- Total supply
-
- {numberFormat.format(tokenInfo.market_data.total_supply)}
-
-
- )}
- {Boolean(tokenInfo.market_data.max_supply) && (
-
- Max supply
-
- {numberFormat.format(tokenInfo.market_data.max_supply)}
-
-
- )}
-
-
-
-
- Historical Price
-
-
- {Boolean(tokenInfo.market_data.current_price.usd) && (
-
- Current price
-
- {currencyFormat.format(tokenInfo.market_data.current_price.usd)}
-
- {numberFormat.format(tokenInfo.market_data.price_change_percentage_24h_in_currency.usd)}
- %
-
-
- (24h change)
-
-
-
- )}
- {Boolean(tokenInfo.market_data.ath.usd) && (
-
- All time height
-
- {currencyFormat.format(tokenInfo.market_data.ath.usd)}
-
- {numberFormat.format(tokenInfo.market_data.ath_change_percentage.usd)}%
-
-
-
-
- {new Date(
- tokenInfo.market_data.ath_date.usd
- ).toLocaleDateString()}
-
-
-
-
- )}
- {Boolean(tokenInfo.market_data.atl.usd) && (
-
- All time low
-
- {currencyFormat.format(tokenInfo.market_data.atl.usd)}
-
- {numberFormat.format(tokenInfo.market_data.atl_change_percentage.usd)}%
-
-
-
-
- {new Date(
- tokenInfo.market_data.atl_date.usd
- ).toLocaleDateString()}
-
-
-
-
- )}
-
+
-
-
- Description
-
-
-
-
- {tokenInfo.description.en}
-
-
-
-
-
- Categories
-
-
-
- {tokenInfo.categories.map((categorie, i) => (
- {categorie}
- ))}
-
-
+
)}
@@ -541,7 +314,6 @@ export const TokenDetailDesktopContainer = (props: {
Market datas from Coingeeko API
Last update: {new Date(tokenInfo?.market_data?.last_updated||new Date ().toLocaleDateString()).toLocaleString()}
-
diff --git a/src/containers/mobile/TokenDetailMobileContainer.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
index c90e1520..533d9cf9 100644
--- a/src/containers/mobile/TokenDetailMobileContainer.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -35,6 +35,8 @@ import { airplane, chevronDown, download, paperPlane } from "ionicons/icons";
import { DataItem } from "@/components/ui/LightChart";
import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
+import { TokenDetailMarketDetail } from "@/components/ui/TokenDetailMarketData";
+import { TokenDetailDescription } from "@/components/ui/TokenDetailDescription";
const LightChart = lazy(() => import("@/components/ui/LightChart"));
@@ -268,247 +270,25 @@ export const TokenDetailMobileContainer = (props: {
{tokenInfo && (
-
-
-
-
- Market details
-
-
- {/*
- Categories
-
- {tokenInfo.categories.map((categorie, i) => (
- {categorie}
- ))}
-
- */}
- {tokenInfo.market_data.market_cap.usd && (
-
- Market Cap.
-
- ${tokenInfo.market_data.market_cap.usd.toFixed(0)}
-
-
- )}
- {tokenInfo.market_data.fully_diluted_valuation.usd && (
-
-
- Fully Diluted Valuation
-
-
- $
- {tokenInfo.market_data.fully_diluted_valuation.usd.toFixed(
- 0
- )}
-
-
- )}
- {tokenInfo.market_data.circulating_supply && (
-
- Circulating supply
-
- {tokenInfo.market_data.circulating_supply.toFixed(0)}
-
-
- )}
- {tokenInfo.market_data.total_supply && (
-
- Total supply
-
- {tokenInfo.market_data.total_supply.toFixed(0)}
-
-
- )}
- {tokenInfo.market_data.max_supply && (
-
- Max supply
-
- {tokenInfo.market_data.max_supply.toFixed(0)}
-
-
- )}
-
-
- Historical Price
-
-
- {tokenInfo.market_data.current_price.usd && (
-
- Current price
-
- ${tokenInfo.market_data.current_price.usd}
-
- {tokenInfo.market_data.price_change_percentage_24h_in_currency.usd.toFixed(
- 2
- )}
- %
-
-
- (24h change)
-
-
-
- )}
- {tokenInfo.market_data.ath.usd && (
-
- All time height
-
- ${tokenInfo.market_data.ath.usd}
-
- {tokenInfo.market_data.ath_change_percentage.usd.toFixed(
- 2
- )}
- %
-
-
-
-
- {new Date(
- tokenInfo.market_data.ath_date.usd
- ).toLocaleDateString()}
-
-
-
-
- )}
- {tokenInfo.market_data.atl.usd && (
-
- All time low
-
- ${tokenInfo.market_data.atl.usd}
-
- {tokenInfo.market_data.atl_change_percentage.usd.toFixed(
- 2
- )}
- %
-
-
-
-
- {new Date(
- tokenInfo.market_data.atl_date.usd
- ).toLocaleDateString()}
-
-
-
-
- )}
-
-
-
-
- Market datas from Coingeeko API
- Last update: {new Date(tokenInfo?.market_data?.last_updated||new Date ().toLocaleDateString()).toLocaleString()}
-
-
-
-
-
+ <>
+
+
+
+
+
+
+ >
)}
+
+
+
+
+ Market datas from Coingeeko API
+ Last update: {new Date(tokenInfo?.market_data?.last_updated||new Date ().toLocaleDateString()).toLocaleString()}
+
+
+
+
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
index 4021a85c..63d06fa8 100644
--- a/src/containers/mobile/WalletMobileContainer.tsx
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -30,13 +30,19 @@ import {
IonItemOption,
IonAlert,
IonModal,
+ IonButton,
+ IonMenuToggle,
} from "@ionic/react";
-import { card, download, paperPlane, repeat } from "ionicons/icons";
+import { card, download, paperPlane, repeat, settings, settingsOutline } from "ionicons/icons";
import { useState } from "react";
import { IAsset } from "@/interfaces/asset.interface";
import { SwapMobileContainer } from "@/containers/mobile/SwapMobileContainer";
import { TokenDetailMobileContainer } from "@/containers/mobile/TokenDetailMobileContainer";
import { EarnMobileContainer } from "@/containers/mobile/EarnMobileContainer";
+import { MenuSettings } from "@/components/ui/MenuSetting";
+import { currencyFormat } from "@/utils/currencyFormat";
+import { Haptics, ImpactStyle } from '@capacitor/haptics';
+import { TokenInfo } from "@/utils/getTokenInfo";
type SelectedTokenDetail =
| {
@@ -58,6 +64,8 @@ interface WalletMobileComProps {
) => void;
isAlertOpen: boolean;
setIsAlertOpen: (value: boolean) => void;
+ isSettingOpen: boolean;
+ setIsSettingOpen: (value: boolean) => void;
}
class WalletMobileContainer extends WalletBaseComponent<
@@ -67,6 +75,41 @@ class WalletMobileContainer extends WalletBaseComponent<
super(props);
}
+ async setIsSwapModalOpen(state?: SelectedTokenDetail | boolean | undefined) {
+ if (state !== false) {
+ await Haptics.impact({ style: ImpactStyle.Light });
+ }
+ this.props.setIsSwapModalOpen(state);
+ }
+
+ async setIsSettingOpen(state: boolean ) {
+ if (state !== false) {
+ await Haptics.impact({ style: ImpactStyle.Light });
+ }
+ this.props.setIsSettingOpen(state);
+ }
+
+ async handleDepositClick(state?: boolean | undefined) {
+ if (state !== false) {
+ await Haptics.impact({ style: ImpactStyle.Light });
+ }
+ await super.handleDepositClick(state);
+ }
+
+ async handleTokenDetailClick(token?: any): Promise {
+ super.handleTokenDetailClick(token);
+ }
+
+ async handleEarnClick(): Promise {
+ await Haptics.impact({ style: ImpactStyle.Light });
+ super.handleEarnClick();
+ }
+
+ async handleTransferClick(state: boolean): Promise {
+ await Haptics.impact({ style: ImpactStyle.Light });
+ await super.handleTransferClick(state);
+ }
+
render() {
return (
<>
@@ -88,9 +131,18 @@ class WalletMobileContainer extends WalletBaseComponent<
fontWeight: "normal",
}}
>
- $ {this.state.totalBalance.toFixed(2)}
+ {currencyFormat.format(this.state.totalBalance)}
+
+ this.setIsSettingOpen(true)}>
+
+
+
@@ -113,7 +165,7 @@ class WalletMobileContainer extends WalletBaseComponent<
margin: "0px 0px 1.5rem",
}}
>
- $ {getReadableValue(this.state.totalBalance)}
+ {currencyFormat.format(this.state.totalBalance)}
@@ -123,7 +175,7 @@ class WalletMobileContainer extends WalletBaseComponent<
this.setState(state)}
setIsSwapModalOpen={() =>
- this.props.setIsSwapModalOpen(true)
+ this.setIsSwapModalOpen(true)
}
/>
@@ -155,14 +207,7 @@ class WalletMobileContainer extends WalletBaseComponent<
{this.state.totalBalance <= 0 && (
-
-
- this.props.setIsAlertOpen(false)
- }
- >
+
@@ -275,7 +320,7 @@ class WalletMobileContainer extends WalletBaseComponent<
- $ {asset.balanceUsd.toFixed(2)}
+ {currencyFormat.format(asset.balanceUsd)}
{asset.balance.toFixed(6)}
@@ -307,7 +352,7 @@ class WalletMobileContainer extends WalletBaseComponent<
{
- this.props.setIsSwapModalOpen(asset);
+ this.setIsSwapModalOpen(asset);
}}
>
{
- this.props.setIsSwapModalOpen(undefined);
+ this.setIsSwapModalOpen(undefined);
}}
>
this.setState(state)}
- setIsSwapModalOpen={() => this.props.setIsSwapModalOpen(true)}
+ setIsSwapModalOpen={() => this.setIsSwapModalOpen(true)}
data={this.state.selectedTokenDetail}
dismiss={() => this.handleTokenDetailClick(null)}
/>
)}
+ this.setIsSettingOpen(false)}
+ >
+
+
+
{super.render()}
>
);
@@ -382,6 +436,7 @@ const withStore = (
return function WalletMobileContainerWithStore() {
const { walletAddress, assets, isMagicWallet } =
Store.useState(getWeb3State);
+ const [isSettingOpen, setIsSettingOpen] = useState(false);
const [isAlertOpen, setIsAlertOpen] = useState(false);
const [isSwapModalOpen, setIsSwapModalOpen] = useState<
SelectedTokenDetail | boolean | undefined
@@ -396,6 +451,8 @@ const withStore = (
setIsAlertOpen={setIsAlertOpen}
isSwapModalOpen={isSwapModalOpen}
setIsSwapModalOpen={(value) => setIsSwapModalOpen(value)}
+ isSettingOpen={isSettingOpen}
+ setIsSettingOpen={setIsSettingOpen}
modalOpts={{
initialBreakpoint: 1,
breakpoints: [0, 1],
From 345d53b395ae2e7d562ce6bca450aeff65a42ffb Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Sat, 16 Mar 2024 02:25:38 +0100
Subject: [PATCH 13/74] refactor: handle state and redirect
---
src/components/Welcome.tsx | 4 ++--
src/containers/mobile/WelcomeMobileContainer.tsx | 8 ++++++++
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/components/Welcome.tsx b/src/components/Welcome.tsx
index 35047792..bfaf8513 100644
--- a/src/components/Welcome.tsx
+++ b/src/components/Welcome.tsx
@@ -60,7 +60,7 @@ export function Welcome({
color="gradient"
style={{marginTop: '2rem'}}
onClick={(e) => {
- router.push("swap");
+ router.push("wallet");
handleSegmentChange({ detail: { value: "wallet" } });
}}
>
@@ -671,7 +671,7 @@ export function Welcome({
size="large"
color="gradient"
onClick={(e) =>{
- router.push("swap");
+ router.push("wallet");
handleSegmentChange({ detail: { value: "wallet" } })
}}
>
diff --git a/src/containers/mobile/WelcomeMobileContainer.tsx b/src/containers/mobile/WelcomeMobileContainer.tsx
index 8f34b968..3079edf3 100644
--- a/src/containers/mobile/WelcomeMobileContainer.tsx
+++ b/src/containers/mobile/WelcomeMobileContainer.tsx
@@ -3,10 +3,18 @@ import { IonPage, } from '@ionic/react';
import ConnectButton from "../../components/ConnectButton";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
+import { useEffect } from "react";
export default function WelcomeMobileContainer() {
const { walletAddress } = Store.useState(getWeb3State);
const router = useIonRouter();
+
+ useEffect(()=> {
+ if (walletAddress) {
+ router.push('wallet')
+ }
+ }, [walletAddress]);
+
return (
From bfcf55f8fcbd4582eac8cb1473319c6a540c1477 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Sat, 16 Mar 2024 02:50:44 +0100
Subject: [PATCH 14/74] fix: rmv toSorted that thow error on old safari
---
src/components/base/WalletBaseContainer.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/base/WalletBaseContainer.tsx b/src/components/base/WalletBaseContainer.tsx
index e01f0c76..aa06658d 100644
--- a/src/components/base/WalletBaseContainer.tsx
+++ b/src/components/base/WalletBaseContainer.tsx
@@ -78,7 +78,7 @@ export default class WalletBaseComponent extends React.Component<
groupAssets() {
const assetGroup = this.props.assets
- .toSorted((a, b) => b.balanceUsd - a.balanceUsd)
+ .sort((a, b) => b.balanceUsd - a.balanceUsd)
.reduce((acc, asset) => {
// check existing asset symbol
const symbol = (asset.name.toLowerCase().includes('aave') && asset.name.toLowerCase() !== 'aave token')
From 20407b7afa7540e57f5752c08f8f384c6b0c52ee Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Sat, 16 Mar 2024 03:13:00 +0100
Subject: [PATCH 15/74] fix: rmv haptic that thow errror on ios
---
src/containers/mobile/WalletMobileContainer.tsx | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
index 63d06fa8..8432241c 100644
--- a/src/containers/mobile/WalletMobileContainer.tsx
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -41,8 +41,6 @@ import { TokenDetailMobileContainer } from "@/containers/mobile/TokenDetailMobil
import { EarnMobileContainer } from "@/containers/mobile/EarnMobileContainer";
import { MenuSettings } from "@/components/ui/MenuSetting";
import { currencyFormat } from "@/utils/currencyFormat";
-import { Haptics, ImpactStyle } from '@capacitor/haptics';
-import { TokenInfo } from "@/utils/getTokenInfo";
type SelectedTokenDetail =
| {
@@ -76,23 +74,14 @@ class WalletMobileContainer extends WalletBaseComponent<
}
async setIsSwapModalOpen(state?: SelectedTokenDetail | boolean | undefined) {
- if (state !== false) {
- await Haptics.impact({ style: ImpactStyle.Light });
- }
this.props.setIsSwapModalOpen(state);
}
async setIsSettingOpen(state: boolean ) {
- if (state !== false) {
- await Haptics.impact({ style: ImpactStyle.Light });
- }
this.props.setIsSettingOpen(state);
}
async handleDepositClick(state?: boolean | undefined) {
- if (state !== false) {
- await Haptics.impact({ style: ImpactStyle.Light });
- }
await super.handleDepositClick(state);
}
@@ -101,12 +90,10 @@ class WalletMobileContainer extends WalletBaseComponent<
}
async handleEarnClick(): Promise {
- await Haptics.impact({ style: ImpactStyle.Light });
super.handleEarnClick();
}
async handleTransferClick(state: boolean): Promise {
- await Haptics.impact({ style: ImpactStyle.Light });
await super.handleTransferClick(state);
}
From 8f7e66860a89c6f40060dee75f2dd0a9f9f65e73 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Sat, 16 Mar 2024 10:25:57 +0100
Subject: [PATCH 16/74] fix: sorting imutable
---
src/components/base/WalletBaseContainer.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/base/WalletBaseContainer.tsx b/src/components/base/WalletBaseContainer.tsx
index aa06658d..9b0a9c92 100644
--- a/src/components/base/WalletBaseContainer.tsx
+++ b/src/components/base/WalletBaseContainer.tsx
@@ -77,9 +77,9 @@ export default class WalletBaseComponent extends React.Component<
}
groupAssets() {
- const assetGroup = this.props.assets
- .sort((a, b) => b.balanceUsd - a.balanceUsd)
- .reduce((acc, asset) => {
+ const assetGroup = [...this.props.assets]
+ ?.sort((a, b) => b.balanceUsd - a.balanceUsd)
+ ?.reduce((acc, asset) => {
// check existing asset symbol
const symbol = (asset.name.toLowerCase().includes('aave') && asset.name.toLowerCase() !== 'aave token')
? asset.name.split(' ').pop()||asset.symbol
From dbfda9397b28ea026688f92b675bd6ce9d3cdd73 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 11:46:30 +0100
Subject: [PATCH 17/74] refactor: hide scrollbar y
---
src/styles/global.scss | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/styles/global.scss b/src/styles/global.scss
index c3b7d0b4..a9f67450 100755
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -33,6 +33,18 @@ p {
color-scheme: light dark;
}
+// Hide scrollbar for Chrome, Safari and Opera
+ion-content::part(scroll)::-webkit-scrollbar {
+ display: none;
+}
+
+// Hide scrollbar for IE, Edge and Firefox
+ion-content::part(scroll) {
+ -ms-overflow-style: none; // IE and Edge
+ scrollbar-width: none; // Firefox
+}
+
+
.header-background {
-webkit-backdrop-filter: blur(20px)!important;
backdrop-filter: blur(20px)!important;
From ee6620b3fb12cf653866f25d0e2b3bdd8c1cec82 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 11:46:48 +0100
Subject: [PATCH 18/74] refactor: fix sont size breaking rules
---
src/containers/mobile/WalletMobileContainer.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
index 8432241c..33b38915 100644
--- a/src/containers/mobile/WalletMobileContainer.tsx
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -109,7 +109,7 @@ class WalletMobileContainer extends WalletBaseComponent<
display: "flex",
}}
>
-
+
Wallet
Date: Mon, 18 Mar 2024 11:47:10 +0100
Subject: [PATCH 19/74] feat: add copy address and switch network features
---
src/containers/DepositContainer.tsx | 88 ++++++++++++++++++++++++++---
1 file changed, 79 insertions(+), 9 deletions(-)
diff --git a/src/containers/DepositContainer.tsx b/src/containers/DepositContainer.tsx
index 17dacfef..3de27ddd 100644
--- a/src/containers/DepositContainer.tsx
+++ b/src/containers/DepositContainer.tsx
@@ -1,4 +1,4 @@
-import { CHAIN_AVAILABLES } from "@/constants/chains";
+import { CHAIN_AVAILABLES, CHAIN_DEFAULT } from "@/constants/chains";
import { getQrcodeAsSVG } from "@/servcies/qrcode.service";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
@@ -13,20 +13,82 @@ import {
IonRow,
IonText,
IonToolbar,
+ useIonModal,
} from "@ionic/react";
import { useEffect, useState } from "react";
-import { scan } from 'ionicons/icons';
+import { copyOutline, scan } from 'ionicons/icons';
+import { SuccessCopyAddress } from "@/components/SuccessCopyAddress";
+import { useLoader } from "@/context/LoaderContext";
+import { SelectNetwork } from "@/components/SelectNetwork";
export const DepositContainer = () => {
const {
- web3Provider,
currentNetwork,
walletAddress,
- connectWallet,
- disconnectWallet,
switchNetwork,
+ isMagicWallet
} = Store.useState(getWeb3State);
const [qrCodeSVG, setQrCodeSVG] = useState(null);
+ const chain =
+ CHAIN_AVAILABLES.find((chain) => chain.id === currentNetwork) ||
+ CHAIN_DEFAULT;
+ const [presentSuccessCopyAddress, dismissSuccessCopyAddress] = useIonModal(
+ () => (
+
+ )
+ );
+ const [presentSelectNetwork, dismissSelectNetwork] = useIonModal(() => (
+
+ ));
+ const { display: displayLoader, hide: hidLoader } = useLoader();
+
+ const handleActions = async (type: string, payload: string) => {
+ await displayLoader();
+ switch (true) {
+ case type === "copy": {
+ navigator?.clipboard?.writeText(payload);
+ // display toast confirmation
+ presentSuccessCopyAddress({
+ cssClass: "modalAlert",
+ onDidDismiss(event) {
+ console.log("onDidDismiss", event.detail.role);
+ if (!event.detail.role || event?.detail?.role === "cancel") return;
+ handleActions(event.detail.role, payload);
+ },
+ });
+ break;
+ }
+ case type === "selectNetwork": {
+ presentSelectNetwork({
+ cssClass: "modalAlert",
+ onDidDismiss(event) {
+ if (!event.detail.role || event?.detail?.role === "cancel") return;
+ handleActions(event.detail.role, event.detail.data).then(() =>
+ hidLoader()
+ );
+ },
+ });
+ break;
+ }
+ case type === "getAddressFromNetwork": {
+ await switchNetwork(Number(payload));
+ dismissSelectNetwork(null, "cancel");
+ await handleActions("copy", `${walletAddress}`);
+ break;
+ }
+ default:
+ break;
+ }
+ await hidLoader();
+ };
useEffect(() => {
if (!walletAddress) {
@@ -60,10 +122,18 @@ export const DepositContainer = () => {
-
-
- {walletAddress}
-
+
+ Wallet Address
+
+ handleActions("copy", walletAddress || "")} style={{ cursor: "pointer" }}>
+ {walletAddress?.slice(0, 6)}...{walletAddress?.slice(walletAddress.length - 6, walletAddress.length)}
+
+
+
+
From 588607560eab630ecc6a7312f1f028ae2e031969 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 12:02:48 +0100
Subject: [PATCH 20/74] refactor: use thumb if exist
---
src/containers/mobile/TokenDetailMobileContainer.tsx | 6 +++++-
src/containers/mobile/WalletMobileContainer.tsx | 10 +++++++---
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/containers/mobile/TokenDetailMobileContainer.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
index 533d9cf9..71d2e6ea 100644
--- a/src/containers/mobile/TokenDetailMobileContainer.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -126,7 +126,11 @@ export const TokenDetailMobileContainer = (props: {
}}
>
{
From a00b72b21ba79ce20c22720ae396357e882509e7 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 12:30:25 +0100
Subject: [PATCH 21/74] refactor: desktop ui image and headers
---
src/components/ReserveDetail.tsx | 26 ++++++++++++++++---
src/components/ui/WalletAssetEntity.tsx | 4 ++-
.../desktop/TokenDetailDesktopContainer.tsx | 8 +++---
3 files changed, 31 insertions(+), 7 deletions(-)
diff --git a/src/components/ReserveDetail.tsx b/src/components/ReserveDetail.tsx
index ad31c732..66852971 100644
--- a/src/components/ReserveDetail.tsx
+++ b/src/components/ReserveDetail.tsx
@@ -504,9 +504,22 @@ export function ReserveDetail(props: IReserveDetailProps) {
return (
<>
-
+
- Market details
+ Pool {pool?.symbol}
+
+ {
+ CHAIN_AVAILABLES.find(
+ (c) => c.id === pool.chainId
+ )?.name
+ }{" "} Network
+
-
+
+
+
+ Market Details
+
+
{
From 5b754b1640e32278bc9a49e251e00c227fa7d716 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 12:43:05 +0100
Subject: [PATCH 22/74] refactor: lock pwa ui
---
src/components/AppShell.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index c40e2319..6f6035f9 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -85,9 +85,9 @@ const DefaultLoadingPage = () => {
}
const isMobilePWADevice =
- Boolean(isPlatform("pwa") && !isPlatform("desktop")) ||
- Boolean(isPlatform("mobileweb")) ||
- Boolean(isPlatform("mobile"));
+ localStorage.getItem('hexa-lite_is-pwa') ||
+ Boolean(isPlatform("pwa")) && !Boolean(isPlatform("mobileweb")) ||
+ Boolean(isPlatform("mobile")) && !Boolean(isPlatform("mobileweb"));
const AppShell = () => {
// get params from url `s=`
From f76604af5f722c9fc6b92bc3270e22e236fd93eb Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 12:48:17 +0100
Subject: [PATCH 23/74] refactor: handle filter empty result
---
src/containers/desktop/WalletDesktopContainer.tsx | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
index 6499d2e7..a3715a4b 100644
--- a/src/containers/desktop/WalletDesktopContainer.tsx
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -277,6 +277,16 @@ class WalletDesktopContainer extends WalletBaseComponent {
/>
);
})}
+
+ {(this.state.assetGroup.filter((asset) =>
+ this.state.filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(this.state.filterBy.toLowerCase())
+ : true
+ ).length === 0) && (
+ Assets not found in your wallet
+ )}
>
From e2846d6b66e3c3a61762991bc005e3d66efaf908 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 12:54:45 +0100
Subject: [PATCH 24/74] refactor: disable action if balance <= 0
---
src/components/mobile/ActionNavButtons.tsx | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/components/mobile/ActionNavButtons.tsx b/src/components/mobile/ActionNavButtons.tsx
index bfc459e9..9a2c8570 100644
--- a/src/components/mobile/ActionNavButtons.tsx
+++ b/src/components/mobile/ActionNavButtons.tsx
@@ -1,3 +1,5 @@
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
import {
IonCol,
IonFab,
@@ -31,11 +33,10 @@ export const MobileActionNavButtons = (props: {
setIsSwapModalOpen,
} = props;
- const modalOpts: Omit &
- HookOverlayOptions = {
- initialBreakpoint: 0.98,
- breakpoints: [0, 0.98],
- };
+ const {assets} = Store.useState(getWeb3State);
+ const balance = assets.reduce((prev, curr) => {
+ return prev + curr.balance
+ }, 0);
return (
@@ -44,6 +45,7 @@ export const MobileActionNavButtons = (props: {
setState({ isTransferModalOpen: true })}
>
@@ -66,6 +68,7 @@ export const MobileActionNavButtons = (props: {
setIsSwapModalOpen()}
>
@@ -78,6 +81,7 @@ export const MobileActionNavButtons = (props: {
{
setState({ isEarnModalOpen: true });
}}
From cfed24beaee4431edf53c0dbf59b8f517388eb38 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 13:42:06 +0100
Subject: [PATCH 25/74] refactor: disable btn on click
---
src/components/ConnectButton.tsx | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/components/ConnectButton.tsx b/src/components/ConnectButton.tsx
index bce9a99a..49cc44a2 100644
--- a/src/components/ConnectButton.tsx
+++ b/src/components/ConnectButton.tsx
@@ -2,6 +2,7 @@ import Store from "@/store";
import { useLoader } from "../context/LoaderContext";
import { IonButton, IonSkeletonText, useIonToast } from "@ionic/react";
import { getWeb3State } from "@/store/selectors";
+import { MouseEvent } from "react";
const ConnectButton = (props: {
style?: any;
@@ -57,7 +58,15 @@ const ConnectButton = (props: {
expand={props?.expand || undefined}
disabled={web3Provider === null}
color="gradient"
- onClick={handleConnect}
+ onClick={async ($event)=> {
+ $event.currentTarget.disabled = true;
+ try {
+ await handleConnect();
+ $event.currentTarget.disabled = false;
+ } catch (err: any) {
+ $event.currentTarget.disabled = false;
+ }
+ }}
>
{web3Provider === null ? (
From 30d2119901f41d122700debc37f8cb746bb6a21b Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 14:22:04 +0100
Subject: [PATCH 26/74] refactor: add more item to setting page
---
src/components/ui/MenuSetting.tsx | 120 +++++++++++++++++++++++++++---
1 file changed, 111 insertions(+), 9 deletions(-)
diff --git a/src/components/ui/MenuSetting.tsx b/src/components/ui/MenuSetting.tsx
index 3b7987a3..39c789bd 100644
--- a/src/components/ui/MenuSetting.tsx
+++ b/src/components/ui/MenuSetting.tsx
@@ -13,8 +13,9 @@ import {
IonButton,
IonModal,
IonFooter,
+ IonNote,
} from "@ionic/react";
-import { radioButtonOn, ribbonOutline } from "ionicons/icons";
+import { open, openOutline, radioButtonOn, ribbonOutline } from "ionicons/icons";
import { getAddressPoints } from "@/servcies/datas.service";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
@@ -54,15 +55,116 @@ export const MenuSettings: React.FC = ({}) => {
+
+ Connected
+
+
+
+
+
+ Gouvernance
+
+
+
+ Snapshot
+
+
+
+ {
+ window.open('https://snapshot.org/#/hexaonelabs.eth', '_blank')
+ }}>
+
+
+
+
+
+
+ Source code
+
+
+
+ Github
+
+
+
+ {
+ window.open('https://github.com/hexaonelabs', '_blank')
+ }}>
+
+
+
+
+
+
+ Terms & Conditions
+
+
+
+ PDF
+
+
+
+ {
+ window.open('https://hexa-lite.io/terms-conditions.pdf', '_blank')
+ }}>
+
+
+
+
+
+
+ Wallet key export
+
+
+
+ Wallet Magik
+
+
+
+ {
+ window.open('https://wallet.magic.link/', '_blank')
+ }}>
+
+
+
+
+
+
+ Version
+
+
+
+ https://hexa-lite.io
+
+
+
+
+ {process.env.NEXT_PUBLIC_APP_VERSION}
+
+ {process.env.NEXT_PUBLIC_APP_BUILD_DATE}
+
+
-
-
- {`HexaLite v${process.env.NEXT_PUBLIC_APP_VERSION} - ${process.env.NEXT_PUBLIC_APP_BUILD_DATE}`}
-
-
From eefa40747888b7abee37046b330a71ddefca231d Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 14:28:32 +0100
Subject: [PATCH 27/74] fix: bg color container
---
src/containers/mobile/WalletMobileContainer.tsx | 4 +++-
src/styles/global.scss | 2 ++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
index 3cff88b2..92cebeb4 100644
--- a/src/containers/mobile/WalletMobileContainer.tsx
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -109,7 +109,9 @@ class WalletMobileContainer extends WalletBaseComponent<
display: "flex",
}}
>
-
+
Wallet
Date: Mon, 18 Mar 2024 14:33:22 +0100
Subject: [PATCH 28/74] fix: change conditions
---
src/components/AppShell.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index 6f6035f9..48f070c3 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -86,7 +86,7 @@ const DefaultLoadingPage = () => {
const isMobilePWADevice =
localStorage.getItem('hexa-lite_is-pwa') ||
- Boolean(isPlatform("pwa")) && !Boolean(isPlatform("mobileweb")) ||
+ Boolean(isPlatform("pwa")) ||
Boolean(isPlatform("mobile")) && !Boolean(isPlatform("mobileweb"));
const AppShell = () => {
From daf4ff270f7eceab4caeb2ef166bda1b3ab58a0a Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Mon, 18 Mar 2024 14:48:11 +0100
Subject: [PATCH 29/74] fix: prevent viewport scale from mobile
---
src/pages/_app.tsx | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index b5fef242..0648c904 100755
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -58,11 +58,9 @@ function MyApp({ Component, pageProps }: AppProps) {
-
-
-
+
+
{/* */}
Date: Mon, 18 Mar 2024 16:21:08 +0100
Subject: [PATCH 30/74] feat: send token implementation
---
public/assets/images/0xFazio.jpeg | Bin 0 -> 33337 bytes
src/components/AppShell.tsx | 2 +
src/containers/TransferContainer.tsx | 9 ++
src/containers/desktop/AboutContainer.tsx | 108 ++++++++++++++++++++++
src/network/Bitcoin.ts | 3 +
src/network/Cosmos.ts | 4 +
src/network/EVM.ts | 18 ++++
src/network/MagicWallet.ts | 1 +
src/network/Solana.ts | 4 +
9 files changed, 149 insertions(+)
create mode 100644 public/assets/images/0xFazio.jpeg
create mode 100644 src/containers/desktop/AboutContainer.tsx
diff --git a/public/assets/images/0xFazio.jpeg b/public/assets/images/0xFazio.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..c348698de1ae62c49f77be6a00604700a1751934
GIT binary patch
literal 33337
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 dci~~#
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
literal 0
HcmV?d00001
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index 48f070c3..0e97b4b7 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -40,6 +40,7 @@ import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { IonRoute } from "@ionic/react";
import { isPlatform } from "@ionic/core";
+import { AboutContainer } from "@/containers/desktop/AboutContainer";
setupIonicReact({ mode: "ios" });
@@ -169,6 +170,7 @@ const AppShell = () => {
)}
/>
} />
+ } />
(
diff --git a/src/containers/TransferContainer.tsx b/src/containers/TransferContainer.tsx
index 43beff85..4ca67892 100644
--- a/src/containers/TransferContainer.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -334,6 +334,9 @@ export const TransferContainer = () => {
const isValid = inputFromAmount > 0 && inputToAddress && inputToAddress.length > 0;
+ const handleSend = async () => {
+ console.log(inputFromAmount, inputToAddress)
+ }
return (
<>
@@ -357,6 +360,7 @@ export const TransferContainer = () => {
assets={assets}
inputFromAmount={inputFromAmount}
setInputFromAmount={setInputFromAmount}
+
/>
@@ -408,6 +412,11 @@ export const TransferContainer = () => {
{
+ $event.currentTarget.disabled = true;
+ await handleSend().catch((err: any) => err);
+ $event.currentTarget.disabled = false;
+ }}
>Send
diff --git a/src/containers/desktop/AboutContainer.tsx b/src/containers/desktop/AboutContainer.tsx
new file mode 100644
index 00000000..368419f7
--- /dev/null
+++ b/src/containers/desktop/AboutContainer.tsx
@@ -0,0 +1,108 @@
+import { IonAvatar, IonButton, IonCol, IonContent, IonGrid, IonIcon, IonPage, IonRow, IonText } from "@ionic/react";
+import { logoTwitter } from "ionicons/icons";
+
+export function AboutContainer() {
+
+ const teams = [
+ {
+ avatar: '',
+ name: 'Fazio Nicolas',
+ sumbStatus: 'Founder',
+ post: 'Chief Executive Officer',
+ links: [
+ {
+ icon: logoTwitter,
+ url: './assets/images/0xFazio.jpeg'
+ }
+ ]
+ }
+ ]
+ return (
+
+
+
+
+
+
+
+
+ Built by the community, for the community
+
+
+
+
+ Our mission is developing products and services that reduce friction and growth web3 adoption.
+
+
+
+
+ image
+
+
+
+
+
+
+
+ Meet the team
+
+
+
+ {teams.map(t => (
+
+
+
+
+
+
+ {t.name}
+ {t.sumbStatus}
+
+
+
+
+ {t.post}
+
+
+ {t.links.map(l => (
+ window.open(l.url, '_blank')}>
+
+
+ ))}
+
+ ))}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/network/Bitcoin.ts b/src/network/Bitcoin.ts
index a0ed82c3..376ad220 100644
--- a/src/network/Bitcoin.ts
+++ b/src/network/Bitcoin.ts
@@ -34,4 +34,7 @@ export class BitcoinWalletUtils extends MagicWalletUtils {
throw new Error("loadBalances() - Method not implemented.");
}
+ async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
+ throw new Error("sendToken() - Method not implemented.");
+ }
}
\ No newline at end of file
diff --git a/src/network/Cosmos.ts b/src/network/Cosmos.ts
index 6f43031a..ada8de3e 100644
--- a/src/network/Cosmos.ts
+++ b/src/network/Cosmos.ts
@@ -131,4 +131,8 @@ export class CosmosWalletUtils extends MagicWalletUtils {
async loadBalances() {
throw new Error("loadBalances() - Method not implemented.");
}
+
+ async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
+ throw new Error("sendToken() - Method not implemented.");
+ }
}
\ No newline at end of file
diff --git a/src/network/EVM.ts b/src/network/EVM.ts
index 363aa58d..1a200591 100644
--- a/src/network/EVM.ts
+++ b/src/network/EVM.ts
@@ -55,6 +55,24 @@ export class EVMWalletUtils extends MagicWalletUtils {
this.assets = assets;
}
+ async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
+ if(!this.web3Provider) {
+ throw new Error("Web3Provider is not initialized");
+ }
+ try {
+ const signer = this.web3Provider.getSigner();
+ const amount = ethers.utils.parseEther(decimalAmount.toString()); // Convert 1 ether to wei
+ const contract = new ethers.Contract(contactAddress, ["function transfer(address, uint256)"], signer);
+ const tx = await contract.transfer(destination, amount);
+ // Wait for transaction to be mined
+ const receipt = await tx.wait();
+ return receipt;
+ } catch (err: any) {
+ console.error(err);
+ throw new Error("Error during sending token");
+ }
+ }
+
private async _setMetamaskNetwork() {
if (!this.web3Provider) {
throw new Error("Web3Provider is not initialized");
diff --git a/src/network/MagicWallet.ts b/src/network/MagicWallet.ts
index db6b1bed..d56e3148 100644
--- a/src/network/MagicWallet.ts
+++ b/src/network/MagicWallet.ts
@@ -32,6 +32,7 @@ export abstract class MagicWalletUtils {
public abstract web3Provider: Web3ProviderType | null;
public abstract isMagicWallet: boolean;
public abstract loadBalances(): Promise;
+ public abstract sendToken(destination: string, decimalAmount: number, contactAddress: string): Promise;
protected abstract _initializeWeb3(): Promise;
/**
diff --git a/src/network/Solana.ts b/src/network/Solana.ts
index ff0628a2..300f9f54 100644
--- a/src/network/Solana.ts
+++ b/src/network/Solana.ts
@@ -42,4 +42,8 @@ export class SolanaWalletUtils extends MagicWalletUtils {
async loadBalances() {
throw new Error("loadBalances() - Method not implemented.");
}
+
+ async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
+ throw new Error("sendToken() - Method not implemented.");
+ }
}
\ No newline at end of file
From 2003c430ea25c6e65831c9d9fd9fb21ebb5f5200 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Tue, 19 Mar 2024 15:00:15 +0100
Subject: [PATCH 31/74] refactor: add force loadAssets
---
src/components/base/WalletBaseContainer.tsx | 12 +-
src/components/mobile/WalletAssetsList.tsx | 158 ++++++++++++++++++
src/components/ui/MenuSetting.tsx | 24 ++-
src/containers/DepositContainer.tsx | 30 ++--
src/containers/TransferContainer.tsx | 38 +++--
.../desktop/WalletDesktopContainer.tsx | 8 +-
src/containers/mobile/EarnMobileContainer.tsx | 18 +-
src/containers/mobile/SwapMobileContainer.tsx | 45 +++--
.../mobile/TokenDetailMobileContainer.tsx | 13 +-
.../mobile/WalletMobileContainer.tsx | 43 +++--
src/network/EVM.ts | 8 +-
src/network/MagicWallet.ts | 2 +-
src/servcies/ankr.service.ts | 17 +-
src/store/effects/web3.effects.ts | 4 +-
src/store/index.ts | 2 +-
15 files changed, 343 insertions(+), 79 deletions(-)
create mode 100644 src/components/mobile/WalletAssetsList.tsx
diff --git a/src/components/base/WalletBaseContainer.tsx b/src/components/base/WalletBaseContainer.tsx
index 9b0a9c92..a90d9397 100644
--- a/src/components/base/WalletBaseContainer.tsx
+++ b/src/components/base/WalletBaseContainer.tsx
@@ -20,11 +20,12 @@ export interface WalletComponentProps {
HookOverlayOptions;
walletAddress?: string;
assets: IAsset[];
+ loadAssets: (force?: boolean) => Promise;
}
export interface WalletComponentState {
filterBy: string | null;
- assetGroup: any[];
+ assetGroup: SelectedTokenDetail[];
totalBalance: number;
selectedTokenDetail: SelectedTokenDetail | null;
isEarnModalOpen: boolean;
@@ -141,6 +142,10 @@ export default class WalletBaseComponent extends React.Component<
});
}
+ async handleRefresh(){
+ this.props.loadAssets(true);
+ }
+
render(): React.ReactNode {
return (
<>
@@ -150,7 +155,7 @@ export default class WalletBaseComponent extends React.Component<
initialBreakpoint={this.props.modalOpts.initialBreakpoint}
onDidDismiss={() => this.handleTransferClick(false)}
>
-
+ this.handleTransferClick(false)} />
extends React.Component<
initialBreakpoint={this.props.modalOpts.initialBreakpoint}
onDidDismiss={() => this.handleDepositClick(false)}
>
-
+ this.handleDepositClick(false)}
+ />
>
diff --git a/src/components/mobile/WalletAssetsList.tsx b/src/components/mobile/WalletAssetsList.tsx
new file mode 100644
index 00000000..bf933ff4
--- /dev/null
+++ b/src/components/mobile/WalletAssetsList.tsx
@@ -0,0 +1,158 @@
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import {
+ IonAvatar,
+ IonCol,
+ IonIcon,
+ IonItem,
+ IonItemOption,
+ IonItemOptions,
+ IonItemSliding,
+ IonLabel,
+ IonList,
+ IonRow,
+ IonText,
+} from "@ionic/react";
+import { SelectedTokenDetail } from "../base/WalletBaseContainer";
+import { getAssetIconUrl } from "@/utils/getAssetIconUrl";
+import { currencyFormat } from "@/utils/currencyFormat";
+import { paperPlane, repeat } from "ionicons/icons";
+
+export const WalletAssetsList = (props: {
+ totalBalance: number;
+ assetGroup: SelectedTokenDetail[];
+ filterBy: string;
+ handleTokenDetailClick: (asset: SelectedTokenDetail) => Promise;
+ handleTransferClick: (asset: SelectedTokenDetail) => Promise;
+ setIsSwapModalOpen: (asset: SelectedTokenDetail) => Promise;
+}) => {
+ const {
+ totalBalance, assetGroup, filterBy,
+ handleTokenDetailClick, handleTransferClick, setIsSwapModalOpen
+ } = props;
+ const { walletAddress, assets, isMagicWallet, loadAssets } =
+ Store.useState(getWeb3State);
+
+ return (
+ <>
+ {totalBalance > 0 && (
+
+
+
+ {assetGroup
+ .filter((asset) =>
+ filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(filterBy.toLowerCase())
+ : true
+ )
+ .sort((a, b) => (a.balanceUsd > b.balanceUsd ? -1 : 1))
+ .map((asset, index) => (
+
+ {
+ console.log("handleTokenDetailClick: ", asset);
+ handleTokenDetailClick(asset);
+ }}
+ >
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+
+ {asset.symbol}
+
+
+
+ {asset.name}
+
+
+
+
+
+ {currencyFormat.format(asset.balanceUsd)}
+
+
+ {asset.balance.toFixed(6)}
+
+
+
+
+ {
+ // close the sliding item after clicking the option
+ (event.target as HTMLElement)
+ .closest("ion-item-sliding")
+ ?.close();
+ }}
+ >
+ {
+ handleTransferClick(asset);
+ }}
+ >
+
+
+ {
+ setIsSwapModalOpen(asset);
+ }}
+ >
+
+
+
+
+ ))}
+
+
+
+ )}
+ >
+ );
+};
diff --git a/src/components/ui/MenuSetting.tsx b/src/components/ui/MenuSetting.tsx
index 39c789bd..5a521790 100644
--- a/src/components/ui/MenuSetting.tsx
+++ b/src/components/ui/MenuSetting.tsx
@@ -14,18 +14,20 @@ import {
IonModal,
IonFooter,
IonNote,
+ IonButtons,
} from "@ionic/react";
-import { open, openOutline, radioButtonOn, ribbonOutline } from "ionicons/icons";
+import { close, open, openOutline, radioButtonOn, ribbonOutline } from "ionicons/icons";
import { getAddressPoints } from "@/servcies/datas.service";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import ConnectButton from "../ConnectButton";
import DisconnectButton from "../DisconnectButton";
-interface MenuSettingsProps {}
+interface MenuSettingsProps {
+ dismiss: ()=> void
+}
-export const MenuSettings: React.FC = ({}) => {
- const menuRef = useRef(null);
+export const MenuSettings: React.FC = ({dismiss}) => {
const { walletAddress } = Store.useState(getWeb3State);
const [points, setPoints] = useState(null);
@@ -34,15 +36,25 @@ export const MenuSettings: React.FC = ({}) => {
Settings
+
+ {
+ dismiss();
+ }}>
+
+
+
-
+
{
- menuRef.current?.close();
+ dismiss();
}}
>
diff --git a/src/containers/DepositContainer.tsx b/src/containers/DepositContainer.tsx
index 3de27ddd..5c64b96a 100644
--- a/src/containers/DepositContainer.tsx
+++ b/src/containers/DepositContainer.tsx
@@ -4,6 +4,7 @@ import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import {
IonButton,
+ IonButtons,
IonCol,
IonContent,
IonFooter,
@@ -12,16 +13,19 @@ import {
IonIcon,
IonRow,
IonText,
+ IonTitle,
IonToolbar,
useIonModal,
} from "@ionic/react";
import { useEffect, useState } from "react";
-import { copyOutline, scan } from 'ionicons/icons';
+import { close, copyOutline, scan } from 'ionicons/icons';
import { SuccessCopyAddress } from "@/components/SuccessCopyAddress";
import { useLoader } from "@/context/LoaderContext";
import { SelectNetwork } from "@/components/SelectNetwork";
-export const DepositContainer = () => {
+export const DepositContainer = (props: {
+ dismiss: ()=> Promise;
+}) => {
const {
currentNetwork,
walletAddress,
@@ -109,15 +113,19 @@ export const DepositContainer = () => {
<>
-
-
-
-
- Deposit
-
-
-
-
+
+ Deposit
+
+
+ {
+ props.dismiss();
+ }}>
+
+
+
diff --git a/src/containers/TransferContainer.tsx b/src/containers/TransferContainer.tsx
index 4ca67892..48b92de7 100644
--- a/src/containers/TransferContainer.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -3,6 +3,7 @@ import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import {
IonButton,
+ IonButtons,
IonCol,
IonContent,
IonFab,
@@ -19,6 +20,7 @@ import {
IonPopover,
IonRow,
IonText,
+ IonTitle,
IonToolbar,
} from "@ionic/react";
import { chevronDown, close, scan } from "ionicons/icons";
@@ -326,8 +328,9 @@ const InputAssetWithDropDown = (props: {
);
};
-export const TransferContainer = () => {
- const { walletAddress, isMagicWallet, assets } = Store.useState(getWeb3State);
+export const TransferContainer = (props: {dismiss: () => Promise;}) => {
+
+ const { walletAddress, isMagicWallet, assets, loadAssets } = Store.useState(getWeb3State);
const [inputFromAmount, setInputFromAmount] = useState(0);
const [inputToAddress, setInputToAddress] = useState(undefined);
const [isScanModalOpen, setIsScanModalOpen] = useState(false);
@@ -335,32 +338,39 @@ export const TransferContainer = () => {
const isValid = inputFromAmount > 0 && inputToAddress && inputToAddress.length > 0;
const handleSend = async () => {
- console.log(inputFromAmount, inputToAddress)
+ console.log(inputFromAmount, inputToAddress);
+ // Todo...
+ // finalize with reload asset list
+ await loadAssets(true);
}
return (
<>
-
-
-
-
- Send token
-
-
-
-
+
+ Send token
+
+
+ {
+ props.dismiss();
+ }}>
+
+
+
-
+
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
index a3715a4b..a3fe6a62 100644
--- a/src/containers/desktop/WalletDesktopContainer.tsx
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -316,10 +316,14 @@ class WalletDesktopContainer extends WalletBaseComponent {
const withStore = (Component: React.ComponentClass) => {
// use named function to prevent re-rendering failure
return function WalletDesktopContainerWithStore() {
- const { walletAddress, assets } = Store.useState(getWeb3State);
+ const { walletAddress, assets, loadAssets } = Store.useState(getWeb3State);
return (
-
+ loadAssets(force)} />
);
};
};
diff --git a/src/containers/mobile/EarnMobileContainer.tsx b/src/containers/mobile/EarnMobileContainer.tsx
index 1aadf67b..fc882be0 100644
--- a/src/containers/mobile/EarnMobileContainer.tsx
+++ b/src/containers/mobile/EarnMobileContainer.tsx
@@ -1,9 +1,12 @@
import {
IonAvatar,
+ IonButton,
+ IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
+ IonIcon,
IonItem,
IonLabel,
IonList,
@@ -31,8 +34,11 @@ import { CHAIN_AVAILABLES } from "@/constants/chains";
import { getReadableValue } from "@/utils/getReadableValue";
import { ETHLiquidStakingstrategyCard } from "../../components/ETHLiquidStakingstrategy";
import { MATICLiquidStakingstrategyCard } from "../../components/MATICLiquidStakingstrategy";
+import { close } from "ionicons/icons";
-export const EarnMobileContainer = () => {
+export const EarnMobileContainer = (props: {
+ dismiss: ()=> Promise;
+}) => {
const [segment, setSegment] = useState("loan");
const { walletAddress } = Store.useState(getWeb3State);
const userSummaryAndIncentivesGroup = Store.useState(
@@ -87,6 +93,16 @@ export const EarnMobileContainer = () => {
Earn
+
+ {
+ props.dismiss();
+ }}>
+
+
+
diff --git a/src/containers/mobile/SwapMobileContainer.tsx b/src/containers/mobile/SwapMobileContainer.tsx
index 97d7fa26..a542dffe 100644
--- a/src/containers/mobile/SwapMobileContainer.tsx
+++ b/src/containers/mobile/SwapMobileContainer.tsx
@@ -1,9 +1,14 @@
import {
+ IonButton,
+ IonButtons,
IonCol,
IonContent,
IonGrid,
+ IonHeader,
+ IonIcon,
IonRow,
IonText,
+ IonToolbar,
useIonToast,
} from "@ionic/react";
import {
@@ -24,6 +29,7 @@ import { LiFiWidgetDynamic } from "../../components/LiFiWidgetDynamic";
import { useLoader } from "@/context/LoaderContext";
import { LIFI_CONFIG } from "../../servcies/lifi.service";
import { IAsset } from "@/interfaces/asset.interface";
+import { close } from "ionicons/icons";
export const SwapMobileContainer = (props: {
token?: {
@@ -34,7 +40,8 @@ export const SwapMobileContainer = (props: {
balanceUsd: number;
thumbnail: string;
assets: IAsset[];
- }
+ },
+ dismiss: ()=> void;
}) => {
const {
web3Provider,
@@ -43,6 +50,7 @@ export const SwapMobileContainer = (props: {
connectWallet,
disconnectWallet,
switchNetwork,
+ loadAssets,
} = Store.useState(getWeb3State);
const widgetEvents = useWidgetEvents();
const { display: displayLoader, hide: hideLoader } = useLoader();
@@ -64,6 +72,7 @@ export const SwapMobileContainer = (props: {
return;
}
await addAddressPoints(walletAddress, data);
+ await loadAssets(true);
};
const onRouteExecutionFailed = (update: RouteExecutionUpdate) => {
console.log("[INFO] onRouteExecutionFailed fired.", update);
@@ -175,14 +184,30 @@ export const SwapMobileContainer = (props: {
};
return (
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+ {
+ props.dismiss();
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/src/containers/mobile/TokenDetailMobileContainer.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
index 71d2e6ea..ed050722 100644
--- a/src/containers/mobile/TokenDetailMobileContainer.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -6,6 +6,7 @@ import {
IonAvatar,
IonBadge,
IonButton,
+ IonButtons,
IonChip,
IonCol,
IonContent,
@@ -31,7 +32,7 @@ import { ethers } from "ethers";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { CHAIN_AVAILABLES } from "@/constants/chains";
-import { airplane, chevronDown, download, paperPlane } from "ionicons/icons";
+import { airplane, chevronDown, close, download, paperPlane } from "ionicons/icons";
import { DataItem } from "@/components/ui/LightChart";
import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
@@ -105,6 +106,16 @@ export const TokenDetailMobileContainer = (props: {
$ {data.balanceUsd.toFixed(2)}
+
+ {
+ props.dismiss();
+ }}>
+
+
+
@@ -139,7 +131,20 @@ class WalletMobileContainer extends WalletBaseComponent<
fullscreen={true}
className="ion-no-padding"
style={{ "--padding-top": "0px" }}
- >
+ >
+ )=> {
+ await super.handleRefresh();
+ setTimeout(() => {
+ // Any calls to load data go here
+ $event.detail.complete();
+ }, 2000);
+ }}>
+
+
+
@@ -186,6 +191,7 @@ class WalletMobileContainer extends WalletBaseComponent<
+
)}
+
@@ -371,7 +378,7 @@ class WalletMobileContainer extends WalletBaseComponent<
initialBreakpoint={this.props.modalOpts.initialBreakpoint}
onDidDismiss={() => this.setState({ isEarnModalOpen: false })}
>
-
+ this.setState({ isEarnModalOpen: false })} />
this.props.setIsSwapModalOpen(false)}
token={
typeof this.props.isSwapModalOpen !== "boolean"
? this.props.isSwapModalOpen
@@ -413,7 +421,7 @@ class WalletMobileContainer extends WalletBaseComponent<
initialBreakpoint={this.props.modalOpts.initialBreakpoint}
onDidDismiss={() => this.setIsSettingOpen(false)}
>
-
+ this.setIsSettingOpen(false)} />
{super.render()}
@@ -427,7 +435,7 @@ const withStore = (
) => {
// use named function to prevent re-rendering failure
return function WalletMobileContainerWithStore() {
- const { walletAddress, assets, isMagicWallet } =
+ const { walletAddress, assets, isMagicWallet, loadAssets } =
Store.useState(getWeb3State);
const [isSettingOpen, setIsSettingOpen] = useState(false);
const [isAlertOpen, setIsAlertOpen] = useState(false);
@@ -450,6 +458,7 @@ const withStore = (
initialBreakpoint: 1,
breakpoints: [0, 1],
}}
+ loadAssets={(force?: boolean)=> loadAssets(force)}
/>
);
};
diff --git a/src/network/EVM.ts b/src/network/EVM.ts
index 1a200591..9d27bbfd 100644
--- a/src/network/EVM.ts
+++ b/src/network/EVM.ts
@@ -4,10 +4,10 @@ import { getTokensBalances } from "../servcies/ankr.service";
import { MagicWalletUtils } from "./MagicWallet";
import { getMagic } from "@/servcies/magic";
-const fetchUserAssets = async (walletAddress: string) => {
+const fetchUserAssets = async (walletAddress: string, force?: boolean) => {
console.log(`[INFO] fetchUserAssets()`, walletAddress);
if (!walletAddress) return null;
- const assets = await getTokensBalances([], walletAddress);
+ const assets = await getTokensBalances([], walletAddress, force);
return assets;
};
@@ -48,9 +48,9 @@ export class EVMWalletUtils extends MagicWalletUtils {
}
}
- async loadBalances() {
+ async loadBalances(force?: boolean) {
if (!this.walletAddress) return;
- const assets = await fetchUserAssets(this.walletAddress);
+ const assets = await fetchUserAssets(this.walletAddress, force);
if (!assets) return;
this.assets = assets;
}
diff --git a/src/network/MagicWallet.ts b/src/network/MagicWallet.ts
index d56e3148..c8384dd1 100644
--- a/src/network/MagicWallet.ts
+++ b/src/network/MagicWallet.ts
@@ -31,7 +31,7 @@ export abstract class MagicWalletUtils {
public abstract web3Provider: Web3ProviderType | null;
public abstract isMagicWallet: boolean;
- public abstract loadBalances(): Promise;
+ public abstract loadBalances(force?: boolean): Promise;
public abstract sendToken(destination: string, decimalAmount: number, contactAddress: string): Promise;
protected abstract _initializeWeb3(): Promise;
diff --git a/src/servcies/ankr.service.ts b/src/servcies/ankr.service.ts
index c976d636..34494111 100644
--- a/src/servcies/ankr.service.ts
+++ b/src/servcies/ankr.service.ts
@@ -37,14 +37,17 @@ const formatingTokensBalances = (assets: IAnkrTokenReponse[], address: string, c
});
}
-const getCachedData = async (key: string) => {
+const getCachedData = async (key: string, force?: boolean) => {
const data = localStorage.getItem(key);
if (!data) {
return null;
}
// check expiration cache using timestamp 10 minutes
const parsedData = JSON.parse(data);
- if (Date.now() - parsedData.timestamp > 600000) {
+ if (Date.now() - parsedData.timestamp > 1000 * 10 && !force) {
+ return null;
+ }
+ if (Date.now() - parsedData.timestamp > 1000 * 0.5 && force) {
return null;
}
console.log('[INFO] {ankrFactory} data from cache: ', parsedData.data);
@@ -63,11 +66,13 @@ const setCachedData = async (key: string, data: any) => {
* @param address wallet address to get balances
* @returns object with balances property that contains an array of TokenInterface
*/
-export const getTokensBalances = async (chainIds: number[], address: string) => {
+export const getTokensBalances = async (chainIds: number[], address: string, force?: boolean) => {
const KEY = `hexa-ankr-service-${address}`;
- const cachedData = await getCachedData(KEY);
- if (cachedData) {
- return cachedData;
+ if (!force) {
+ const cachedData = await getCachedData(KEY);
+ if (cachedData) {
+ return cachedData;
+ }
}
const APP_ANKR_APIKEY = process.env.NEXT_PUBLIC_APP_ANKR_APIKEY;
const chainsList =
diff --git a/src/store/effects/web3.effects.ts b/src/store/effects/web3.effects.ts
index 48e543ed..8030d050 100644
--- a/src/store/effects/web3.effects.ts
+++ b/src/store/effects/web3.effects.ts
@@ -35,8 +35,8 @@ export const initializeWeb3 = async (chainId: number = CHAIN_DEFAULT.id) => {
switchNetwork: async (chainId: number) => {
await initializeWeb3(chainId);
},
- loadAssets: async () => {
- await magicUtils.loadBalances().catch((err) => {
+ loadAssets: async (force?: boolean) => {
+ await magicUtils.loadBalances(force).catch((err) => {
console.error('[ERROR] {{Web3Effect}} load balance error: ', err?.message ? err.message : err);
});
setWeb3State({
diff --git a/src/store/index.ts b/src/store/index.ts
index 5250c7b1..98c5b51c 100755
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -14,7 +14,7 @@ export interface IWeb3State {
connectWallet(ops?: {email: string;}): Promise;
disconnectWallet(): Promise;
switchNetwork: (chainId: number) => Promise;
- loadAssets: () => Promise;
+ loadAssets: (force?: boolean) => Promise;
}
export interface IPoolsState {
From 6bef92e8e57431768c6a192a4b240db8a43bdcf2 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Tue, 19 Mar 2024 16:34:52 +0100
Subject: [PATCH 32/74] feat: add transfer native token
---
src/containers/TransferContainer.tsx | 68 ++++++++++++++++++++--------
src/network/EVM.ts | 47 +++++++++++++++++--
src/servcies/ankr.service.ts | 16 ++++---
src/store/effects/web3.effects.ts | 12 +++++
src/store/index.ts | 12 +++++
5 files changed, 126 insertions(+), 29 deletions(-)
diff --git a/src/containers/TransferContainer.tsx b/src/containers/TransferContainer.tsx
index 48b92de7..84078cd2 100644
--- a/src/containers/TransferContainer.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -142,14 +142,17 @@ const InputAssetWithDropDown = (props: {
assets: IAsset[];
inputFromAmount: number;
setInputFromAmount: Dispatch>;
+ setInputFromAsset: Dispatch>;
}) => {
- const { assets, setInputFromAmount, inputFromAmount } = props;
+ const { assets, setInputFromAmount, inputFromAmount, setInputFromAsset } = props;
const [errorMessage, setErrorMessage] = useState();
const [selectedAsset, setSelectedAsset] = useState(assets[0]);
const [isLoading, setIsLoading] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
const popover = useRef(null);
+ setInputFromAsset(selectedAsset);
+
const maxBalance = useMemo(() => {
// round to the lower tenth
return Math.floor(selectedAsset?.balance * 10000) / 10000;
@@ -213,6 +216,7 @@ const InputAssetWithDropDown = (props: {
>
{
setPopoverOpen(false);
setSelectedAsset(asset);
+ setInputFromAsset(asset)
setInputFromAmount(() => 0);
setErrorMessage(() => undefined);
// setQuote(() => undefined);
@@ -249,6 +254,7 @@ const InputAssetWithDropDown = (props: {
>
Promise;}) => {
- const { walletAddress, isMagicWallet, assets, loadAssets } = Store.useState(getWeb3State);
+ const { walletAddress, isMagicWallet, assets, loadAssets, transfer, switchNetwork, currentNetwork } = Store.useState(getWeb3State);
const [inputFromAmount, setInputFromAmount] = useState(0);
const [inputToAddress, setInputToAddress] = useState(undefined);
+ const [inputFromAsset, setInputFromAsset] = useState(undefined);
const [isScanModalOpen, setIsScanModalOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
- const isValid = inputFromAmount > 0 && inputToAddress && inputToAddress.length > 0;
+ const isValid = inputFromAmount > 0
+ && inputToAddress
+ && inputToAddress.length > 0
+ && inputFromAsset?.contractAddress;
const handleSend = async () => {
- console.log(inputFromAmount, inputToAddress);
- // Todo...
- // finalize with reload asset list
- await loadAssets(true);
+ console.log('handleSend: ', {inputFromAmount, inputToAddress, inputFromAsset});
+ if (inputFromAmount && inputToAddress && inputFromAsset?.contractAddress){
+ if (inputFromAsset?.chain?.id && inputFromAsset?.chain?.id !== currentNetwork) {
+ await switchNetwork(inputFromAsset?.chain?.id)
+ }
+ await transfer(
+ {inputFromAmount, inputToAddress, inputFromAsset: inputFromAsset.contractAddress}
+ )
+ // finalize with reload asset list
+ await loadAssets(true);
+ }
}
return (
<>
@@ -365,31 +383,43 @@ export const TransferContainer = (props: {dismiss: () => Promise;}) => {
-
+
+
+
+
+ Currently only support native token transfer
+
+
+
+
+
+
+ Token
+
a.type === 'NATIVE')}
inputFromAmount={inputFromAmount}
setInputFromAmount={setInputFromAmount}
-
+ setInputFromAsset={setInputFromAsset}
/>
-
+
+
+
+ Destination address
+
+
-
- Destination address
-
Promise;}) => {
{
- $event.currentTarget.disabled = true;
+ setIsLoading(true);
await handleSend().catch((err: any) => err);
- $event.currentTarget.disabled = false;
+ setIsLoading(false);
}}
>Send
diff --git a/src/network/EVM.ts b/src/network/EVM.ts
index 9d27bbfd..3ddc93c1 100644
--- a/src/network/EVM.ts
+++ b/src/network/EVM.ts
@@ -60,12 +60,43 @@ export class EVMWalletUtils extends MagicWalletUtils {
throw new Error("Web3Provider is not initialized");
}
try {
+ console.log({
+ destination, decimalAmount, contactAddress
+ })
const signer = this.web3Provider.getSigner();
- const amount = ethers.utils.parseEther(decimalAmount.toString()); // Convert 1 ether to wei
+ const from = await signer.getAddress();
+ const amount = ethers.utils.parseUnits(decimalAmount.toString(), 18); // Convert 1 ether to wei
const contract = new ethers.Contract(contactAddress, ["function transfer(address, uint256)"], signer);
- const tx = await contract.transfer(destination, amount);
- // Wait for transaction to be mined
+
+ const data = contract.interface.encodeFunctionData("transfer", [destination, amount] );
+
+ const tx = await signer.sendTransaction({
+ to: destination,
+ value: amount,
+ // data
+ });
const receipt = await tx.wait();
+ // // Load token contract
+ // const tokenContract = new ethers.Contract(contactAddress, ['function transfer(address, uint256)'], signer);
+
+ // // Send tokens to recipient
+ // const transaction = await tokenContract.transfer(destination, amount);
+ // const receipt = await transaction.wait();
+ // console.log(receipt);
+
+
+
+ //Define the data parameter
+ // const data = contract.interface.encodeFunctionData("transfer", [destination, amount] )
+ // const tx = await signer.sendTransaction({
+ // to: contactAddress,
+ // from,
+ // value: ethers.utils.parseUnits("0.000", "ether"),
+ // data: data
+ // });
+ // // const tx = await contract.transfer(destination, amount);
+ // // Wait for transaction to be mined
+ // const receipt = await tx.wait();
return receipt;
} catch (err: any) {
console.error(err);
@@ -93,4 +124,14 @@ export class EVMWalletUtils extends MagicWalletUtils {
);
}
}
+
+ async estimateGas() {
+ // const limit = await provider.estimateGas({
+ // from: signer.address,
+ // to: tokenContract,
+ // value: ethers.utils.parseUnits("0.000", "ether"),
+ // data: data
+
+ // });
+ }
}
diff --git a/src/servcies/ankr.service.ts b/src/servcies/ankr.service.ts
index 34494111..bd82c9a4 100644
--- a/src/servcies/ankr.service.ts
+++ b/src/servcies/ankr.service.ts
@@ -40,14 +40,17 @@ const formatingTokensBalances = (assets: IAnkrTokenReponse[], address: string, c
const getCachedData = async (key: string, force?: boolean) => {
const data = localStorage.getItem(key);
if (!data) {
+ console.log('No data in cache.')
return null;
}
// check expiration cache using timestamp 10 minutes
const parsedData = JSON.parse(data);
- if (Date.now() - parsedData.timestamp > 1000 * 10 && !force) {
+ if (Date.now() - parsedData.timestamp > 10 * 60 * 1000 && !force) {
+ console.log('Expire cache 10 minute')
return null;
}
- if (Date.now() - parsedData.timestamp > 1000 * 0.5 && force) {
+ if (Date.now() - parsedData.timestamp > 1 * 60 * 1000 && force) {
+ console.log('Expire cache 1 minute')
return null;
}
console.log('[INFO] {ankrFactory} data from cache: ', parsedData.data);
@@ -68,11 +71,10 @@ const setCachedData = async (key: string, data: any) => {
*/
export const getTokensBalances = async (chainIds: number[], address: string, force?: boolean) => {
const KEY = `hexa-ankr-service-${address}`;
- if (!force) {
- const cachedData = await getCachedData(KEY);
- if (cachedData) {
- return cachedData;
- }
+ const cachedData = await getCachedData(KEY, force);
+ console.log('cachedData:', cachedData);
+ if (cachedData) {
+ return cachedData;
}
const APP_ANKR_APIKEY = process.env.NEXT_PUBLIC_APP_ANKR_APIKEY;
const chainsList =
diff --git a/src/store/effects/web3.effects.ts b/src/store/effects/web3.effects.ts
index 8030d050..9f9f89bc 100644
--- a/src/store/effects/web3.effects.ts
+++ b/src/store/effects/web3.effects.ts
@@ -44,6 +44,18 @@ export const initializeWeb3 = async (chainId: number = CHAIN_DEFAULT.id) => {
assets: magicUtils.assets,
});
},
+ transfer: async (ops: {
+ inputFromAmount: number;
+ inputToAddress: string;
+ inputFromAsset: string;
+ }) => {
+ const result = await magicUtils.sendToken(
+ ops.inputToAddress,
+ ops.inputFromAmount,
+ ops.inputFromAsset
+ );
+ console.log('[INFO] {{Web3Effect}} transfer result: ', result);
+ }
};
console.log('[INFO] {{Web3Effect}} state: ', state);
setWeb3State(state);
diff --git a/src/store/index.ts b/src/store/index.ts
index 98c5b51c..dfc14e44 100755
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -15,6 +15,11 @@ export interface IWeb3State {
disconnectWallet(): Promise;
switchNetwork: (chainId: number) => Promise;
loadAssets: (force?: boolean) => Promise;
+ transfer: (ops: {
+ inputFromAmount: number;
+ inputToAddress: string;
+ inputFromAsset: string;
+ }) => Promise;
}
export interface IPoolsState {
@@ -56,6 +61,13 @@ const defaultState: IStore = Object.freeze({
loadAssets: async () => {
throw new Error("loadAssets function not implemented");
},
+ transfer: async (ops: {
+ inputFromAmount: number;
+ inputToAddress: string;
+ inputFromAsset: string;
+ }) => {
+ throw new Error("transfer function not implemented");
+ },
}
});
From d879a1248930fe2d2615ac576b3775ec41c35d52 Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Tue, 19 Mar 2024 21:43:46 +0100
Subject: [PATCH 33/74] feat: onramp integration
---
src/components/AppShell.tsx | 25 +-
src/components/ConnectButton.tsx | 5 +-
src/components/Header.tsx | 2 +-
src/components/base/WalletBaseContainer.tsx | 77 ++-
src/containers/BuyWithFiat.tsx | 39 ++
src/containers/DepositContainer.tsx | 20 +-
src/containers/TransferContainer.tsx | 587 ++++++++++--------
.../desktop/WalletDesktopContainer.tsx | 30 +-
src/containers/mobile/EarnMobileContainer.tsx | 14 +-
.../mobile/WalletMobileContainer.tsx | 40 +-
10 files changed, 538 insertions(+), 301 deletions(-)
create mode 100644 src/containers/BuyWithFiat.tsx
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index 0e97b4b7..56d90311 100755
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -21,6 +21,11 @@ import {
IonItem,
IonAvatar,
IonProgressBar,
+ IonModal,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonButtons,
} from "@ionic/react";
import { StatusBar, Style } from "@capacitor/status-bar";
@@ -41,6 +46,8 @@ import { getWeb3State } from "@/store/selectors";
import { IonRoute } from "@ionic/react";
import { isPlatform } from "@ionic/core";
import { AboutContainer } from "@/containers/desktop/AboutContainer";
+import { close } from "ionicons/icons";
+import { BuyWithFiat } from "@/containers/BuyWithFiat";
setupIonicReact({ mode: "ios" });
@@ -96,6 +103,7 @@ const AppShell = () => {
let segment = pathname.split("/")[1] || "swap"; // urlParams.get("s") || "swap";
const { walletAddress, isMagicWallet } = Store.useState(getWeb3State);
const [presentFiatWarning, dismissFiatWarning] = useIonAlert();
+ const [isBuyWithFiatModalOpen, setIsBuyWithFiatModalOpen] = useState(false);
const isNotFound =
segment && ["wallet", "swap", "fiat", "defi", "earn"].indexOf(segment) === -1;
@@ -103,14 +111,13 @@ const AppShell = () => {
const [currentSegment, setSegment] = useState(segment);
const handleSegmentChange = async (e: any) => {
if (e.detail.value === "fiat") {
- if (walletAddress && walletAddress !== "" && isMagicWallet) {
- const magic = await getMagic();
- magic.wallet.showOnRamp();
+ if (walletAddress && walletAddress !== "") {
+ setIsBuyWithFiatModalOpen(true);
} else {
await presentFiatWarning({
header: "Information",
message:
- "Connect with e-mail or social login to enable buy crypto with fiat.",
+ "Connect to enable buy crypto with fiat.",
buttons: ["OK"],
cssClass: "modalAlert",
});
@@ -190,7 +197,9 @@ const AppShell = () => {
)}
}>
- {currentSegment === "wallet" && ( )}
+ {currentSegment === "wallet" && (
+
+ )}
}>
{currentSegment === "swap" && ( )}
@@ -330,6 +339,12 @@ const AppShell = () => {
)}
+ setIsBuyWithFiatModalOpen(false)}
+ >
+ setIsBuyWithFiatModalOpen(false)} />
+
);
};
diff --git a/src/components/ConnectButton.tsx b/src/components/ConnectButton.tsx
index 49cc44a2..f5b68e75 100644
--- a/src/components/ConnectButton.tsx
+++ b/src/components/ConnectButton.tsx
@@ -64,7 +64,10 @@ const ConnectButton = (props: {
await handleConnect();
$event.currentTarget.disabled = false;
} catch (err: any) {
- $event.currentTarget.disabled = false;
+ console.log('[ERROR] {ConnectButton} handleConnect(): ', err);
+ if ($event.currentTarget) {
+ $event.currentTarget.disabled = false;
+ }
}
}}
>
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 474e29d4..ea35f474 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -110,11 +110,11 @@ export function Header({
mode="ios"
value={currentSegment}
onIonChange={(e: any) => {
- router.push(`/${e.detail.value}`)
if (e.detail.value === 'fiat-segment') {
handleSegmentChange({detail: {value: 'fiat'}});
return;
};
+ router.push(`/${e.detail.value}`)
handleSegmentChange(e);
}}
>
diff --git a/src/components/base/WalletBaseContainer.tsx b/src/components/base/WalletBaseContainer.tsx
index a90d9397..f23cd282 100644
--- a/src/components/base/WalletBaseContainer.tsx
+++ b/src/components/base/WalletBaseContainer.tsx
@@ -1,9 +1,21 @@
-import { IonModal, ModalOptions } from "@ionic/react";
+import {
+ IonButton,
+ IonButtons,
+ IonContent,
+ IonHeader,
+ IonIcon,
+ IonModal,
+ IonTitle,
+ IonToolbar,
+ ModalOptions,
+} from "@ionic/react";
import React from "react";
import { IAsset } from "@/interfaces/asset.interface";
import { DepositContainer } from "@/containers/DepositContainer";
import { HookOverlayOptions } from "@ionic/react/dist/types/hooks/HookOverlayOptions";
import { TransferContainer } from "../../containers/TransferContainer";
+import { close } from "ionicons/icons";
+import { BuyWithFiat } from "@/containers/BuyWithFiat";
export type SelectedTokenDetail = {
name: string;
@@ -31,6 +43,7 @@ export interface WalletComponentState {
isEarnModalOpen: boolean;
isTransferModalOpen: boolean;
isDepositModalOpen: boolean;
+ isBuyWithFiatModalOpen: boolean;
}
export default class WalletBaseComponent extends React.Component<
@@ -47,6 +60,7 @@ export default class WalletBaseComponent extends React.Component<
isEarnModalOpen: false,
isTransferModalOpen: false,
isDepositModalOpen: false,
+ isBuyWithFiatModalOpen: false,
};
}
@@ -82,19 +96,23 @@ export default class WalletBaseComponent extends React.Component<
?.sort((a, b) => b.balanceUsd - a.balanceUsd)
?.reduce((acc, asset) => {
// check existing asset symbol
- const symbol = (asset.name.toLowerCase().includes('aave') && asset.name.toLowerCase() !== 'aave token')
- ? asset.name.split(' ').pop()||asset.symbol
- : asset.symbol;
- const name = (asset.name.toLowerCase().includes('aave') && asset.name.toLowerCase() !== 'aave token')
- ? asset.name.split(' ').pop()||asset.name
- : asset.name;
-
+ const symbol =
+ asset.name.toLowerCase().includes("aave") &&
+ asset.name.toLowerCase() !== "aave token"
+ ? asset.name.split(" ").pop() || asset.symbol
+ : asset.symbol;
+ const name =
+ asset.name.toLowerCase().includes("aave") &&
+ asset.name.toLowerCase() !== "aave token"
+ ? asset.name.split(" ").pop() || asset.name
+ : asset.name;
const index = acc.findIndex((a) => a.symbol === symbol);
if (index !== -1) {
- const balanceUsd = (asset.balanceUsd <= 0 && asset.balance > 0 )
- ? acc[index].priceUsd * asset.balance
- : asset.balanceUsd;
+ const balanceUsd =
+ asset.balanceUsd <= 0 && asset.balance > 0
+ ? acc[index].priceUsd * asset.balance
+ : asset.balanceUsd;
acc[index].balance += asset.balance;
acc[index].balanceUsd += balanceUsd;
acc[index].assets.push(asset);
@@ -110,7 +128,7 @@ export default class WalletBaseComponent extends React.Component<
});
}
return acc;
- }, [] as { name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[] }[])
+ }, [] as { name: string; symbol: string; priceUsd: number; balance: number; balanceUsd: number; thumbnail: string; assets: IAsset[] }[]);
this.setState({ assetGroup });
}
@@ -120,9 +138,9 @@ export default class WalletBaseComponent extends React.Component<
async handleTokenDetailClick(token: any = null) {
console.log(token);
- this.setState((prev) =>({
- ...prev,
- selectedTokenDetail: token
+ this.setState((prev) => ({
+ ...prev,
+ selectedTokenDetail: token,
}));
}
@@ -131,8 +149,8 @@ export default class WalletBaseComponent extends React.Component<
}
async handleTransferClick(state: boolean) {
- console.log('handleTransferClick', state)
- this.setState({isTransferModalOpen: state});
+ console.log("handleTransferClick", state);
+ this.setState({ isTransferModalOpen: state });
}
async handleDepositClick(state?: boolean) {
@@ -142,10 +160,19 @@ export default class WalletBaseComponent extends React.Component<
});
}
- async handleRefresh(){
+ async handleRefresh() {
this.props.loadAssets(true);
}
+ async handleBuyWithFiat(state: boolean) {
+ console.log(">>>>>", state);
+
+ this.setState((prev) => ({
+ ...prev,
+ isBuyWithFiatModalOpen: state,
+ }));
+ }
+
render(): React.ReactNode {
return (
<>
@@ -164,10 +191,20 @@ export default class WalletBaseComponent extends React.Component<
initialBreakpoint={this.props.modalOpts.initialBreakpoint}
onDidDismiss={() => this.handleDepositClick(false)}
>
- this.handleDepositClick(false)}
+
+ this.handleBuyWithFiat(state)
+ }
+ dismiss={() => this.handleDepositClick(false)}
/>
-
+
+ this.handleBuyWithFiat(false)}
+ >
+ this.handleBuyWithFiat(false)} />
+
>
);
}
diff --git a/src/containers/BuyWithFiat.tsx b/src/containers/BuyWithFiat.tsx
new file mode 100644
index 00000000..285f3acb
--- /dev/null
+++ b/src/containers/BuyWithFiat.tsx
@@ -0,0 +1,39 @@
+import { IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonTitle, IonToolbar } from "@ionic/react";
+import { close } from "ionicons/icons";
+
+export const BuyWithFiat = (props: {
+ dismiss: ()=> void
+}) => {
+
+ return (
+ <>
+
+
+
+ Buy
+
+
+ {
+ props.dismiss();
+ }}
+ >
+
+
+
+
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/containers/DepositContainer.tsx b/src/containers/DepositContainer.tsx
index 5c64b96a..aaaa5e6c 100644
--- a/src/containers/DepositContainer.tsx
+++ b/src/containers/DepositContainer.tsx
@@ -25,6 +25,7 @@ import { SelectNetwork } from "@/components/SelectNetwork";
export const DepositContainer = (props: {
dismiss: ()=> Promise;
+ handleBuyWithFiat: (state: boolean)=> Promise;
}) => {
const {
currentNetwork,
@@ -54,10 +55,11 @@ export const DepositContainer = (props: {
));
const { display: displayLoader, hide: hidLoader } = useLoader();
- const handleActions = async (type: string, payload: string) => {
+ const handleActions = async (type: string, payload?: string) => {
await displayLoader();
switch (true) {
case type === "copy": {
+ if (!payload) return;
navigator?.clipboard?.writeText(payload);
// display toast confirmation
presentSuccessCopyAddress({
@@ -88,6 +90,9 @@ export const DepositContainer = (props: {
await handleActions("copy", `${walletAddress}`);
break;
}
+ case type === 'buy': {
+ props.handleBuyWithFiat(true);
+ }
default:
break;
}
@@ -196,6 +201,19 @@ export const DepositContainer = (props: {
*/}
+
+
+ {
+ handleActions('buy');
+ }}>
+ Buy Crypto
+
+
+
>
);
};
diff --git a/src/containers/TransferContainer.tsx b/src/containers/TransferContainer.tsx
index 84078cd2..1a754cfb 100644
--- a/src/containers/TransferContainer.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -25,7 +25,14 @@ import {
} from "@ionic/react";
import { chevronDown, close, scan } from "ionicons/icons";
import { SymbolIcon } from "../components/SymbolIcon";
-import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from "react";
+import {
+ Dispatch,
+ SetStateAction,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
import { CHAIN_AVAILABLES, CHAIN_DEFAULT } from "@/constants/chains";
import { getReadableAmount } from "@/utils/getReadableAmount";
import { InputInputEventDetail, IonInputCustomEvent } from "@ionic/core";
@@ -36,14 +43,19 @@ const isNumberKey = (evt: React.KeyboardEvent) => {
return !(charCode > 31 && (charCode < 48 || charCode > 57));
};
-const scanQrCode = async (html5QrcodeScanner: Html5Qrcode): Promise => {
+const scanQrCode = async (
+ html5QrcodeScanner: Html5Qrcode
+): Promise => {
try {
- const qrboxFunction = function(viewfinderWidth: number, viewfinderHeight: number) {
+ const qrboxFunction = function (
+ viewfinderWidth: number,
+ viewfinderHeight: number
+ ) {
// Square QR Box, with size = 80% of the min edge width.
const size = Math.min(viewfinderWidth, viewfinderHeight) * 0.8;
return {
- width: size,
- height: size
+ width: size,
+ height: size,
};
};
const cameras = await Html5Qrcode.getCameras();
@@ -52,39 +64,48 @@ const scanQrCode = async (html5QrcodeScanner: Html5Qrcode): Promise c.label.includes("back"))?.id || cameras[0].id;
- console.log('>>', cameraId, cameras)
+ const cameraId =
+ cameras.find((c) => c.label.includes("back"))?.id || cameras[0].id;
+ console.log(">>", cameraId, cameras);
// start scanner
- const config = {
+ const config = {
fps: 10,
qrbox: qrboxFunction,
// Important notice: this is experimental feature, use it at your
// own risk. See documentation in
// mebjas@/html5-qrcode/src/experimental-features.ts
experimentalFeatures: {
- useBarCodeDetectorIfSupported: true
+ useBarCodeDetectorIfSupported: true,
},
rememberLastUsedCamera: true,
- showTorchButtonIfSupported: true
+ showTorchButtonIfSupported: true,
};
if (!cameraId) {
throw new Error("No camera found");
}
// If you want to prefer front camera
return new Promise((resolve, reject) => {
- html5QrcodeScanner.start(cameraId, config, (decodedText, decodedResult)=> {
- // stop reader
- html5QrcodeScanner.stop();
- // resolve promise with the decoded text
- resolve(decodedText);
- }, (error) =>{});
+ html5QrcodeScanner.start(
+ cameraId,
+ config,
+ (decodedText, decodedResult) => {
+ // stop reader
+ html5QrcodeScanner.stop();
+ // resolve promise with the decoded text
+ resolve(decodedText);
+ },
+ (error) => {}
+ );
});
} catch (error: any) {
throw new Error(error?.message || "BarcodeScanner not available");
}
};
-const ScanModal = (props: { isOpen: boolean, onDismiss: (address?: string) => void }) => {
+const ScanModal = (props: {
+ isOpen: boolean;
+ onDismiss: (address?: string) => void;
+}) => {
const [html5Qrcode, setHtml5Qrcode] = useState();
const elementRef = useRef(null);
@@ -92,66 +113,80 @@ const ScanModal = (props: { isOpen: boolean, onDismiss: (address?: string) => vo
if (!props.isOpen) {
return;
}
- console.log('>>>>', elementRef.current)
+ console.log(">>>>", elementRef.current);
if (!elementRef.current) {
return;
}
if (!html5Qrcode) {
throw new Error("BarcodeScanner not available");
}
- const scaner = new html5Qrcode('reader-scan-element')
+ const scaner = new html5Qrcode("reader-scan-element");
if (!scaner) {
throw new Error("BarcodeScanner not loaded");
}
try {
- scanQrCode(scaner).then(
- result => {
- scaner.stop();
- props.onDismiss(result);
- }
- );
+ scanQrCode(scaner).then((result) => {
+ scaner.stop();
+ props.onDismiss(result);
+ });
} catch (error: any) {
console.error(error);
scaner.stop();
}
+ return () => {
+ scaner.stop();
+ };
}, [elementRef.current, html5Qrcode, props.isOpen]);
-
+
return (
- {
- import("html5-qrcode").then(
- (m) => setHtml5Qrcode(()=> (m.Html5Qrcode))
- );
+ import("html5-qrcode").then((m) => setHtml5Qrcode(() => m.Html5Qrcode));
}}
- onDidDismiss={()=> props.onDismiss()}>
-
-
+ onDidDismiss={() => props.onDismiss()}
+ >
+
- props.onDismiss()}>
+ props.onDismiss()}
+ >
-
+
- )
+ );
};
const InputAssetWithDropDown = (props: {
assets: IAsset[];
inputFromAmount: number;
setInputFromAmount: Dispatch>;
- setInputFromAsset: Dispatch>;
+ setInputFromAsset: Dispatch>;
}) => {
- const { assets, setInputFromAmount, inputFromAmount, setInputFromAsset } = props;
+ const { assets, setInputFromAmount, inputFromAmount, setInputFromAsset } =
+ props;
const [errorMessage, setErrorMessage] = useState();
const [selectedAsset, setSelectedAsset] = useState(assets[0]);
const [isLoading, setIsLoading] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
- const popover = useRef(null);
+ // const popover = useRef(null);
- setInputFromAsset(selectedAsset);
+ useEffect(() => {
+ if (selectedAsset) {
+ setInputFromAsset(selectedAsset);
+ }
+ return () => {};
+ });
const maxBalance = useMemo(() => {
// round to the lower tenth
@@ -179,202 +214,236 @@ const InputAssetWithDropDown = (props: {
};
return (
-
- {
- $event.stopPropagation();
- // set position
- popover.current!.event = $event;
- // open popover
- setPopoverOpen(() => true);
- }}
- >
-
+
+
-
-
-
+ background: "#0f1629",
+ padding: "0.65rem 0.5rem",
+ borderRadius: "24px",
+ marginBottom: "0.5rem",
+ }}>
+
+ {
+ $event.stopPropagation();
+ // set position
+ // popover.current!.event = $event;
+ // open popover
+ setPopoverOpen(() => true);
+ }}
+ >
+
+
+
+
-
setPopoverOpen(false)}
- >
-
-
-
- Available assets
-
-
-
- {assets
- .filter((a) => a.balance > 0)
- .map((asset, index) => (
- {
- setPopoverOpen(false);
- setSelectedAsset(asset);
- setInputFromAsset(asset)
- setInputFromAmount(() => 0);
- setErrorMessage(() => undefined);
- // setQuote(() => undefined);
- console.log({ selectedAsset });
- }}
- >
-
-
-
-
- {asset.symbol}
-
-
-
- {
- CHAIN_AVAILABLES.find(
- (c) => c.id === asset?.chain?.id
- )?.name
- }
-
-
-
-
- {Number(asset?.balance).toFixed(6)}
-
-
-
- {getReadableAmount(
- +asset?.balance,
- Number(asset?.priceUsd),
- "No deposit"
- )}
-
-
-
-
- ))}
-
-
-
+
+
+ {selectedAsset?.symbol}
+
+ {
+ $event.stopPropagation();
+ setInputFromAmount(() => selectedAsset?.balance || 0);
+ }}
+ >
+ Max: {maxBalance}
+
+
+
+
+
+
+ {
+ if (isNumberKey(e)) {
+ setIsLoading(() => true);
+ }
+ }}
+ onIonInput={(e) => handleInputChange(e)}
+ />
+
+
+
+
-
+
setPopoverOpen(false)}
+ className="modalAlert"
+ >
+
- {selectedAsset?.symbol}
+ Available assets
- {
- $event.stopPropagation();
- setInputFromAmount(() => selectedAsset?.balance || 0);
- }}
- >
- Max :{maxBalance}
-
-
-
-
-
- {
- if (isNumberKey(e)) {
- setIsLoading(() => true);
- }
- }}
- onIonInput={(e) => handleInputChange(e)}
- />
-
-
+
+
+ {assets
+ .filter((a) => a.balance > 0)
+ .map((asset, index) => (
+ {
+ setPopoverOpen(() => false);
+ setSelectedAsset(asset);
+ setInputFromAsset(asset);
+ setInputFromAmount(() => 0);
+ setErrorMessage(() => undefined);
+ // setQuote(() => undefined);
+ console.log({ selectedAsset });
+ }}
+ >
+
+
+
+
+ {asset.symbol}
+
+
+
+ {
+ CHAIN_AVAILABLES.find((c) => c.id === asset?.chain?.id)
+ ?.name
+ }
+
+
+
+
+ {Number(asset?.balance).toFixed(6)}
+
+
+
+ {getReadableAmount(
+ +asset?.balance,
+ Number(asset?.priceUsd),
+ "No deposit"
+ )}
+
+
+
+
+ ))}
+
+
+ >
);
};
-export const TransferContainer = (props: {dismiss: () => Promise;}) => {
-
- const { walletAddress, isMagicWallet, assets, loadAssets, transfer, switchNetwork, currentNetwork } = Store.useState(getWeb3State);
+export const TransferContainer = (props: { dismiss: () => Promise }) => {
+ const {
+ walletAddress,
+ isMagicWallet,
+ assets,
+ loadAssets,
+ transfer,
+ switchNetwork,
+ currentNetwork,
+ } = Store.useState(getWeb3State);
const [inputFromAmount, setInputFromAmount] = useState(0);
- const [inputToAddress, setInputToAddress] = useState(undefined);
- const [inputFromAsset, setInputFromAsset] = useState(undefined);
+ const [inputToAddress, setInputToAddress] = useState(
+ undefined
+ );
+ const [inputFromAsset, setInputFromAsset] = useState(
+ undefined
+ );
const [isScanModalOpen, setIsScanModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const isValid = inputFromAmount > 0
- && inputToAddress
- && inputToAddress.length > 0
- && inputFromAsset?.contractAddress;
+ const isValid =
+ inputFromAmount > 0 &&
+ inputToAddress &&
+ inputToAddress.length > 0 &&
+ inputFromAsset?.contractAddress;
const handleSend = async () => {
- console.log('handleSend: ', {inputFromAmount, inputToAddress, inputFromAsset});
- if (inputFromAmount && inputToAddress && inputFromAsset?.contractAddress){
- if (inputFromAsset?.chain?.id && inputFromAsset?.chain?.id !== currentNetwork) {
- await switchNetwork(inputFromAsset?.chain?.id)
+ console.log("handleSend: ", {
+ inputFromAmount,
+ inputToAddress,
+ inputFromAsset,
+ });
+ if (inputFromAmount && inputToAddress && inputFromAsset?.contractAddress) {
+ if (
+ inputFromAsset?.chain?.id &&
+ inputFromAsset?.chain?.id !== currentNetwork
+ ) {
+ await switchNetwork(inputFromAsset?.chain?.id);
}
- await transfer(
- {inputFromAmount, inputToAddress, inputFromAsset: inputFromAsset.contractAddress}
- )
+ await transfer({
+ inputFromAmount,
+ inputToAddress,
+ inputFromAsset: inputFromAsset.contractAddress,
+ });
// finalize with reload asset list
await loadAssets(true);
}
- }
+ };
return (
<>
-
+
Send token
- {
props.dismiss();
- }}>
+ }}
+ >
@@ -386,83 +455,93 @@ export const TransferContainer = (props: {dismiss: () => Promise;}) => {
-
- Currently only support native token transfer
-
+ Currently only support native token transfer
-
+
Token
a.type === 'NATIVE')}
+ assets={assets.filter((a) => a.type === "NATIVE")}
inputFromAmount={inputFromAmount}
setInputFromAmount={setInputFromAmount}
setInputFromAsset={setInputFromAsset}
/>
-
-
- Destination address
-
-
-
- {
- console.log($event)
- setInputToAddress(() => ($event.detail.value|| undefined));
- }} />
- {
- setIsScanModalOpen(()=> true);
+ Destination address
+
+
+
-
-
-
-
+ {
+ setInputToAddress(
+ () => $event.detail.value || undefined
+ );
+ }}
+ />
+
+
+ {
+ setIsScanModalOpen(() => true);
+ }}
+ >
+
+
+
+
+
+ {
if (data) {
setInputToAddress(() => data);
}
setIsScanModalOpen(() => false);
- }} />
+ }}
+ />
- {
+ disabled={!isValid || isLoading}
+ onClick={async ($event) => {
setIsLoading(true);
await handleSend().catch((err: any) => err);
setIsLoading(false);
}}
- >Send
+ >
+ Send
+
>
-
);
};
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
index a3fe6a62..1259aaba 100644
--- a/src/containers/desktop/WalletDesktopContainer.tsx
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -122,7 +122,9 @@ class WalletDesktopContainer extends WalletBaseComponent {
{}}
+ onClick={() => {
+ super.handleBuyWithFiat(true);
+ }}
>
@@ -132,12 +134,19 @@ class WalletDesktopContainer extends WalletBaseComponent {
- Buy crypto
+
+
+ Buy crypto
+
+
You have to get ETH to use your wallet. Buy with
credit card or with Apple Pay
+
+ Buy crypto
+
@@ -155,7 +164,9 @@ class WalletDesktopContainer extends WalletBaseComponent {
{}}
+ onClick={() => {
+ super.handleDepositClick()
+ }}
>
@@ -165,12 +176,19 @@ class WalletDesktopContainer extends WalletBaseComponent {
- Deposit assets
+
+
+ Deposit assets
+
+
Transfer tokens from another wallet or from a
crypto exchange
+
+ Deposit assets
+
@@ -315,7 +333,9 @@ class WalletDesktopContainer extends WalletBaseComponent {
const withStore = (Component: React.ComponentClass) => {
// use named function to prevent re-rendering failure
- return function WalletDesktopContainerWithStore() {
+ return function WalletDesktopContainerWithStore(props: {
+ // handleDepositClick: (state: boolean) => void;
+ }) {
const { walletAddress, assets, loadAssets } = Store.useState(getWeb3State);
return (
diff --git a/src/containers/mobile/EarnMobileContainer.tsx b/src/containers/mobile/EarnMobileContainer.tsx
index fc882be0..f0bd1352 100644
--- a/src/containers/mobile/EarnMobileContainer.tsx
+++ b/src/containers/mobile/EarnMobileContainer.tsx
@@ -39,7 +39,7 @@ import { close } from "ionicons/icons";
export const EarnMobileContainer = (props: {
dismiss: ()=> Promise;
}) => {
- const [segment, setSegment] = useState("loan");
+ const [segment, setSegment] = useState("earn");
const { walletAddress } = Store.useState(getWeb3State);
const userSummaryAndIncentivesGroup = Store.useState(
getUserSummaryAndIncentivesGroupState
@@ -80,18 +80,18 @@ export const EarnMobileContainer = (props: {
className="ion-padding-vertical"
>
- setSegment(() => "loan")}
- >
- Loan market
-
setSegment(() => "earn")}
>
Earn
+ setSegment(() => "loan")}
+ >
+ Loan market
+
-
+ super.handleBuyWithFiat(true)}>
-
+
- Buy crypto
+
+
+ Buy crypto
+
+
You have to get ETH to use your wallet. Buy
with credit card or with Apple Pay
+
+ Buy crypto
+
@@ -231,16 +238,22 @@ class WalletMobileContainer extends WalletBaseComponent<
-
+
- Deposit assets
+
+
+ Deposit assets
+
+
- You have to get ETH to use your wallet. Buy
- with credit card or with Apple Pay
+ Transefer tokens from another wallet or from a crypto exchange
+
+ Deposit assets
+
@@ -368,6 +381,19 @@ class WalletMobileContainer extends WalletBaseComponent<
)}
+
+
+
+
+ You are using a non-custodial wallet that give you complete
+ control over your cryptocurrency funds and private keys.
+ Unlike custodial wallets, you manage your own security,
+ enhancing privacy and independence in the decentralized
+ cryptocurrency space.
+
+
+
+
From 06d2608e0a14da698f9f4e5ec35d4fc2fa7a052f Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Tue, 19 Mar 2024 23:43:33 +0100
Subject: [PATCH 34/74] refactor: add install modal
---
src/components/Header.tsx | 344 +++++++++++-------
src/components/ui/WalletAssetEntity.tsx | 15 +-
.../desktop/WalletDesktopContainer.tsx | 16 +-
3 files changed, 231 insertions(+), 144 deletions(-)
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index ea35f474..a1edace3 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,17 +1,21 @@
import {
IonButton,
+ IonButtons,
IonCol,
IonGrid,
IonHeader,
IonIcon,
IonImg,
IonMenuToggle,
+ IonModal,
IonPopover,
IonRow,
IonSegment,
IonSegmentButton,
IonText,
+ IonTitle,
IonToolbar,
+ isPlatform,
useIonRouter,
} from "@ionic/react";
import {
@@ -62,6 +66,7 @@ export function Header({
const { walletAddress } = Store.useState(getWeb3State);
const [points, setPoints] = useState(null);
const [isPointsPopoverOpen, setIsPointsPopoverOpen] = useState(false);
+ const [isInstallModalOpen, setIsInstallModalOpen] = useState(false);
const pointsPopoverRef = useRef(null);
const router = useIonRouter();
const openPopover = (e: any) => {
@@ -74,76 +79,141 @@ export function Header({
// render component
return (
-
-
-
-
- {!currentSegment || currentSegment === "welcome" ? (
- <>{currentSegment}>
- ) : (
- <>
-
-
+
+
+
+
+ {!currentSegment || currentSegment === "welcome" ? (
+ <>{currentSegment}>
+ ) : (
+ <>
+
+
+
+
+
+
-
-
-
-
- {
- if (e.detail.value === 'fiat-segment') {
- handleSegmentChange({detail: {value: 'fiat'}});
- return;
- };
- router.push(`/${e.detail.value}`)
- handleSegmentChange(e);
+ position: "absolute",
+ top: "50%",
+ left: "50%",
+ transform: "translate(-50%, -50%)",
}}
+ class="ion-padding ion-hide-md-down"
>
- Wallet
- Exchange
-
- Earn interest
-
-
- Lend & borrow
-
-
- Buy
-
-
-
-
- {walletAddress ? (
- <>
+ {
+ if (e.detail.value === "fiat-segment") {
+ handleSegmentChange({ detail: { value: "fiat" } });
+ return;
+ }
+ router.push(`/${e.detail.value}`);
+ handleSegmentChange(e);
+ }}
+ >
+ Wallet
+ Exchange
+
+ Earn interest
+
+
+ Lend & borrow
+
+
+ Buy
+
+
+
+
+ {walletAddress ? (
+ <>
+
+
openPopover(e)}
+ >
+
+
+ Points
+
+
+
+
+ Connected
+
+
+
+
+
{
+ setPoints(() => null);
+ setIsPointsPopoverOpen(false);
+ }}
+ onWillPresent={async () => {
+ const response = await getAddressPoints(
+ walletAddress
+ ).catch((error) => {});
+ console.log("response", response);
+ if (response?.data?.totalPoints) {
+ setPoints(() => response.data.totalPoints);
+ } else {
+ setPoints(() => "0");
+ }
+ }}
+ >
+ setIsPointsPopoverOpen(false)}
+ />
+
+
+ >
+ ) : (
openPopover(e)}
>
-
-
- Connected
-
-
-
-
-
{
- setPoints(() => null);
- setIsPointsPopoverOpen(false);
- }}
- onWillPresent={async () => {
- const response = await getAddressPoints(
- walletAddress
- ).catch((error) => {});
- console.log("response", response);
- if (response?.data?.totalPoints) {
- setPoints(() => response.data.totalPoints);
- } else {
- setPoints(() => "0");
- }
- }}
- >
- setIsPointsPopoverOpen(false)}
- />
-
+
- >
- ) : (
-
+ )}
+
+ {/* Mobile nav button */}
+
+
-
-
- Points
-
+
-
-
- )}
-
- {/* Mobile nav button */}
-
-
-
-
-
-
-
- >
- )}
+
+
+ >
+ )}
+
+
+ {!isPlatform("pwa") && isPlatform("mobile") && (
+
+ {
+ setIsInstallModalOpen(true);
+ }}>
+
+ Install App
+
+
+
+ )}
+
+
+
+ setIsInstallModalOpen(false)}
+ className="modalAlert"
+ >
+
+
+
+
+
+ Install to enable all features!
+
+
+ Earn strategies, loans market, send, deposit and buy crypto with fiat.
+
+
+
+ {isPlatform('ios') && (
+ <>
+ Tap the Share button at the bottom of the browser
+ Scroll down and select "Add to Home Screen."
+ Tap "Add" in the top right corner
+ >
+ )}
+ {!isPlatform('ios') && (
+ <>
+ - Tap the menu button (three dots) in your browserr
+ - Select "Add to Home Screen" or "Install App."
+ >
+ )}
+
+
+ Enjoy instant access to our app with a single tap!
+
+
+
+ setIsInstallModalOpen(false)}>ok
+
-
-
+
+ >
);
}
diff --git a/src/components/ui/WalletAssetEntity.tsx b/src/components/ui/WalletAssetEntity.tsx
index 87025a1d..c4c5147f 100644
--- a/src/components/ui/WalletAssetEntity.tsx
+++ b/src/components/ui/WalletAssetEntity.tsx
@@ -31,8 +31,12 @@ export function WalletAssetEntity(props: {
>
-
+
{currencyFormat.format(asset.priceUsd)}
-
+
{numberFormat.format(asset.balance)}
-
+
{currencyFormat.format(asset.balanceUsd)}
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
index 1259aaba..23a6e0e9 100644
--- a/src/containers/desktop/WalletDesktopContainer.tsx
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -248,7 +248,9 @@ class WalletDesktopContainer extends WalletBaseComponent {
@@ -256,15 +258,21 @@ class WalletDesktopContainer extends WalletBaseComponent {
Balance
From 0776e10b16c0c26b3052b2bfd462b7cb00607eac Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Tue, 19 Mar 2024 23:53:47 +0100
Subject: [PATCH 35/74] feat: camera selection
---
src/containers/TransferContainer.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/containers/TransferContainer.tsx b/src/containers/TransferContainer.tsx
index 1a754cfb..686a6917 100644
--- a/src/containers/TransferContainer.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -65,7 +65,7 @@ const scanQrCode = async (
// get prefered back camera if available or load the first one
const cameraId =
- cameras.find((c) => c.label.includes("back"))?.id || cameras[0].id;
+ cameras.find((c) => c.label.toLowerCase().includes("rear"))?.id || cameras[0].id;
console.log(">>", cameraId, cameras);
// start scanner
const config = {
From afd5b13b6ab293b516cf08e7c4ddae6687b2b40d Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Wed, 20 Mar 2024 00:17:45 +0100
Subject: [PATCH 36/74] refactor: fees %
---
src/components/FAQ.tsx | 2 +-
src/containers/desktop/SwapContainer.tsx | 2 +-
src/servcies/lifi.service.ts | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/FAQ.tsx b/src/components/FAQ.tsx
index cfe09714..6654f150 100644
--- a/src/components/FAQ.tsx
+++ b/src/components/FAQ.tsx
@@ -61,7 +61,7 @@ export const FAQ: React.FC = () => {
},
{
q: "What are the fees for swapping?",
- a: "Hexa Lite does not charge any fees for swapping. However, the protocols that we integrate with may charge a fee for swapping and on-chain gas fees are applicable according to each network. That's why you have to get some ETH to pay for gas fees on Ethereum network and majors EVM network.",
+ a: "Hexa Lite charge 1% fees for swapping. However, the protocols that we integrate with may charge a fee for swapping and on-chain gas fees are applicable according to each network. That's why you have to get some ETH to pay for gas fees on Ethereum network and majors EVM network.",
},
{
q: "How pay on-chain transaction fees?",
diff --git a/src/containers/desktop/SwapContainer.tsx b/src/containers/desktop/SwapContainer.tsx
index 3788df25..e22f94ad 100644
--- a/src/containers/desktop/SwapContainer.tsx
+++ b/src/containers/desktop/SwapContainer.tsx
@@ -105,7 +105,7 @@ export default function SwapContainer() {
// load environment config
const widgetConfig: WidgetConfig = {
...LIFI_CONFIG,
- fee: 0, // set fee to 0 for main swap feature
+ // fee: 0, // set fee to 0 for main swap feature
walletManagement: {
connect: async () => {
try {
diff --git a/src/servcies/lifi.service.ts b/src/servcies/lifi.service.ts
index 72e70c99..a4f26556 100644
--- a/src/servcies/lifi.service.ts
+++ b/src/servcies/lifi.service.ts
@@ -802,7 +802,7 @@ export const swapWithLiFi = async (
export const LIFI_CONFIG = Object.freeze({
// integrator: "cra-example",
integrator: process.env.NEXT_PUBLIC_APP_IS_PROD ? "hexa-lite" : "",
- fee: 0.05,
+ fee: 0.01,
variant: "expandable",
insurance: true,
containerStyle: {
From 89204922ed6aa782086f1b4c7ce9d67d69758f9c Mon Sep 17 00:00:00 2001
From: FazioNico
Date: Wed, 20 Mar 2024 03:09:20 +0100
Subject: [PATCH 37/74] refactor: welcom desktop ui
---
public/assets/images/preview-app.png | Bin 0 -> 905047 bytes
src/components/Welcome.tsx | 65 ++++++++++++++++++---------
src/styles/global.scss | 57 ++++++++++++++++++++---
3 files changed, 95 insertions(+), 27 deletions(-)
create mode 100644 public/assets/images/preview-app.png
diff --git a/public/assets/images/preview-app.png b/public/assets/images/preview-app.png
new file mode 100644
index 0000000000000000000000000000000000000000..614c68c4442586fa1afccc85b57b8a0adf798082
GIT binary patch
literal 905047
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 LIgN-f
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