From 4b609ddc5fb7874297701b842fe65129b7cd454b Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 14:48:01 +0900 Subject: [PATCH 001/257] [feat] preserve previous mainPage --- .../src/pages/main/index-previous.tsx | 1113 +++++++++++++++++ apps/extension/src/pages/main/index.tsx | 30 +- 2 files changed, 1114 insertions(+), 29 deletions(-) create mode 100644 apps/extension/src/pages/main/index-previous.tsx diff --git a/apps/extension/src/pages/main/index-previous.tsx b/apps/extension/src/pages/main/index-previous.tsx new file mode 100644 index 0000000000..6642f64ff3 --- /dev/null +++ b/apps/extension/src/pages/main/index-previous.tsx @@ -0,0 +1,1113 @@ +import React, { + FunctionComponent, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { observer } from "mobx-react-lite"; +import { useStore } from "../../stores"; +import { + Buttons, + ClaimAll, + CopyAddress, + IBCTransferView, + BuyCryptoModal, + StakeWithKeplrDashboardButton, + UpdateNoteModal, + UpdateNotePageData, +} from "./components"; +import { Stack } from "../../components/stack"; +import { CoinPretty, PricePretty } from "@keplr-wallet/unit"; +import { + ArrowTopRightOnSquareIcon, + EyeIcon, + EyeSlashIcon, +} from "../../components/icon"; +import { Box } from "../../components/box"; +import { Modal } from "../../components/modal"; +import { DualChart } from "./components/chart"; +import { Gutter } from "../../components/gutter"; +import { H1, Subtitle3, Subtitle4 } from "../../components/typography"; +import { ColorPalette, SidePanelMaxWidth } from "../../styles"; +import { AvailableTabView } from "./available"; +import { StakedTabView } from "./staked"; +import { SearchTextInput } from "../../components/input"; +import { animated, useSpringValue, easings } from "@react-spring/web"; +import { defaultSpringConfig } from "../../styles/spring"; +import { IChainInfoImpl, QueryError } from "@keplr-wallet/stores"; +import { Skeleton } from "../../components/skeleton"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useGlobarSimpleBar } from "../../hooks/global-simplebar"; +import styled, { useTheme } from "styled-components"; +import { IbcHistoryView } from "./components/ibc-history-view"; +import { LayeredHorizontalRadioGroup } from "../../components/radio-group"; +import { XAxis, YAxis } from "../../components/axis"; +import { DepositModal } from "./components/deposit-modal"; +import { MainHeaderLayout, MainHeaderLayoutRef } from "./layouts/header"; +import { amountToAmbiguousAverage, isRunningInSidePanel } from "../../utils"; +import { InExtensionMessageRequester } from "@keplr-wallet/router-extension"; +import { + ChainInfoWithCoreTypes, + LogAnalyticsEventMsg, +} from "@keplr-wallet/background"; +import { BACKGROUND_PORT } from "@keplr-wallet/router"; +import { useBuySupportServiceInfos } from "../../hooks/use-buy-support-service-infos"; +import { BottomTabsHeightRem } from "../../bottom-tabs"; +import { DenomHelper } from "@keplr-wallet/common"; +import { NewSidePanelHeaderTop } from "./new-side-panel-header-top"; +import { ModularChainInfo } from "@keplr-wallet/types"; +import { ChainIdHelper } from "@keplr-wallet/cosmos"; +import { AvailableTabLinkButtonList } from "./components/available-tab-link-button-list"; +import { INITIA_CHAIN_ID, NEUTRON_CHAIN_ID } from "../../config.ui"; + +export interface ViewToken { + token: CoinPretty; + chainInfo: IChainInfoImpl | ModularChainInfo; + isFetching: boolean; + error: QueryError | undefined; +} + +export const useIsNotReady = () => { + const { chainStore, queriesStore } = useStore(); + + const query = queriesStore.get(chainStore.chainInfos[0].chainId).cosmos + .queryRPCStatus; + + return query.response == null && query.error == null; +}; + +type TabStatus = "available" | "staked"; + +export const MainPage: FunctionComponent<{ + setIsNotReady: (isNotReady: boolean) => void; +}> = observer(({ setIsNotReady }) => { + const { + analyticsStore, + hugeQueriesStore, + uiConfigStore, + keyRingStore, + priceStore, + } = useStore(); + + const isNotReady = useIsNotReady(); + const intl = useIntl(); + const theme = useTheme(); + + const setIsNotReadyRef = useRef(setIsNotReady); + setIsNotReadyRef.current = setIsNotReady; + useLayoutEffect(() => { + setIsNotReadyRef.current(isNotReady); + }, [isNotReady]); + + const [tabStatus, setTabStatus] = React.useState("available"); + + const disabledViewAssetTokenMap = + uiConfigStore.manageViewAssetTokenConfig.getViewAssetTokenMapByVaultId( + keyRingStore.selectedKeyInfo?.id ?? "" + ); + + const availableTotalPrice = useMemo(() => { + let result: PricePretty | undefined; + for (const bal of hugeQueriesStore.allKnownBalances) { + const disabledCoinSet = disabledViewAssetTokenMap.get( + ChainIdHelper.parse(bal.chainInfo.chainId).identifier + ); + + if ( + bal.price && + !disabledCoinSet?.has(bal.token.currency.coinMinimalDenom) + ) { + if (!result) { + result = bal.price; + } else { + result = result.add(bal.price); + } + } + } + return result; + }, [hugeQueriesStore.allKnownBalances, disabledViewAssetTokenMap]); + const availableTotalPriceEmbedOnlyUSD = useMemo(() => { + let result: PricePretty | undefined; + for (const bal of hugeQueriesStore.allKnownBalances) { + // TODO: 이거 starknet에서도 embedded를 확인할 수 있도록 수정해야함. + if (!("currencies" in bal.chainInfo)) { + continue; + } + if (!(bal.chainInfo.embedded as ChainInfoWithCoreTypes).embedded) { + continue; + } + if (bal.price) { + const price = priceStore.calculatePrice(bal.token, "usd"); + if (price) { + if (!result) { + result = price; + } else { + result = result.add(price); + } + } + } + } + return result; + }, [hugeQueriesStore.allKnownBalances, priceStore]); + const availableChartWeight = (() => { + if (!isNotReady && uiConfigStore.isPrivacyMode) { + if (tabStatus === "available") { + return 1; + } + return 0; + } + + return availableTotalPrice && !isNotReady + ? Number.parseFloat(availableTotalPrice.toDec().toString()) + : 0; + })(); + const stakedTotalPrice = useMemo(() => { + let result: PricePretty | undefined; + for (const bal of hugeQueriesStore.delegations) { + if (bal.price) { + if (!result) { + result = bal.price; + } else { + result = result.add(bal.price); + } + } + } + for (const bal of hugeQueriesStore.unbondings) { + if (bal.price) { + if (!result) { + result = bal.price; + } else { + result = result.add(bal.price); + } + } + } + return result; + }, [hugeQueriesStore.delegations, hugeQueriesStore.unbondings]); + const stakedTotalPriceEmbedOnlyUSD = useMemo(() => { + let result: PricePretty | undefined; + for (const bal of hugeQueriesStore.delegations) { + if (!("currencies" in bal.chainInfo)) { + continue; + } + if (!(bal.chainInfo.embedded as ChainInfoWithCoreTypes).embedded) { + continue; + } + if (bal.price) { + const price = priceStore.calculatePrice(bal.token, "usd"); + if (price) { + if (!result) { + result = price; + } else { + result = result.add(price); + } + } + } + } + for (const bal of hugeQueriesStore.unbondings) { + if (!("currencies" in bal.chainInfo)) { + continue; + } + if (!(bal.chainInfo.embedded as ChainInfoWithCoreTypes).embedded) { + continue; + } + if (bal.price) { + const price = priceStore.calculatePrice(bal.token, "usd"); + if (price) { + if (!result) { + result = price; + } else { + result = result.add(price); + } + } + } + } + return result; + }, [hugeQueriesStore.delegations, hugeQueriesStore.unbondings, priceStore]); + const stakedChartWeight = (() => { + if (!isNotReady && uiConfigStore.isPrivacyMode) { + if (tabStatus === "staked") { + return 1; + } + return 0; + } + + return stakedTotalPrice && !isNotReady + ? Number.parseFloat(stakedTotalPrice.toDec().toString()) + : 0; + })(); + + const lastTotalAvailableAmbiguousAvg = useRef(-1); + const lastTotalStakedAmbiguousAvg = useRef(-1); + useEffect(() => { + if (!isNotReady) { + const totalAvailableAmbiguousAvg = availableTotalPriceEmbedOnlyUSD + ? amountToAmbiguousAverage(availableTotalPriceEmbedOnlyUSD) + : 0; + const totalStakedAmbiguousAvg = stakedTotalPriceEmbedOnlyUSD + ? amountToAmbiguousAverage(stakedTotalPriceEmbedOnlyUSD) + : 0; + if ( + lastTotalAvailableAmbiguousAvg.current !== totalAvailableAmbiguousAvg || + lastTotalStakedAmbiguousAvg.current !== totalStakedAmbiguousAvg + ) { + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + new LogAnalyticsEventMsg("user_properties", { + totalAvailableFiatAvg: totalAvailableAmbiguousAvg, + totalStakedFiatAvg: totalStakedAmbiguousAvg, + id: keyRingStore.selectedKeyInfo?.id, + keyType: keyRingStore.selectedKeyInfo?.insensitive[ + "keyRingType" + ] as string | undefined, + }) + ); + } + lastTotalAvailableAmbiguousAvg.current = totalAvailableAmbiguousAvg; + lastTotalStakedAmbiguousAvg.current = totalStakedAmbiguousAvg; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + availableTotalPriceEmbedOnlyUSD, + isNotReady, + stakedTotalPriceEmbedOnlyUSD, + ]); + + const [isOpenDepositModal, setIsOpenDepositModal] = React.useState(false); + const [isOpenBuy, setIsOpenBuy] = React.useState(false); + + const buySupportServiceInfos = useBuySupportServiceInfos(); + + const searchRef = useRef(null); + const [search, setSearch] = useState(""); + const [isEnteredSearch, setIsEnteredSearch] = useState(false); + useEffect(() => { + // Give focus whenever available tab is selected. + if (!isNotReady && tabStatus === "available") { + // And clear search text. + setSearch(""); + + if (searchRef.current) { + searchRef.current.focus({ + preventScroll: true, + }); + } + } + }, [tabStatus, isNotReady]); + useEffect(() => { + // Log if a search term is entered at least once. + if (isEnteredSearch) { + analyticsStore.logEvent("input_searchAssetOrChain", { + pageName: "main", + }); + } + }, [analyticsStore, isEnteredSearch]); + useEffect(() => { + // Log a search term with delay. + const handler = setTimeout(() => { + if (isEnteredSearch && search) { + analyticsStore.logEvent("input_searchAssetOrChain", { + inputValue: search, + pageName: "main", + }); + } + }, 1000); + + return () => { + clearTimeout(handler); + }; + }, [analyticsStore, search, isEnteredSearch]); + + const searchScrollAnim = useSpringValue(0, { + config: defaultSpringConfig, + }); + const globalSimpleBar = useGlobarSimpleBar(); + + const animatedPrivacyModeHover = useSpringValue(0, { + config: defaultSpringConfig, + }); + + const [isChangelogModalOpen, setIsChangelogModalOpen] = useState(false); + useEffect(() => { + if (uiConfigStore.changelogConfig.showingInfo.length > 0) { + setIsChangelogModalOpen(true); + } + }, [uiConfigStore.changelogConfig.showingInfo.length]); + + const [isRefreshButtonVisible, setIsRefreshButtonVisible] = useState(false); + const [isRefreshButtonLoading, setIsRefreshButtonLoading] = useState(false); + const forcePreventScrollRefreshButtonVisible = useRef(false); + useEffect(() => { + if (!isRunningInSidePanel()) { + return; + } + + const scrollElement = globalSimpleBar.ref.current?.getScrollElement(); + if (scrollElement) { + // 최상단에선 안 보임 + // 그러나 최상단에서 움직임 없이 5초 지나면 보임 + // 스크롤 다운 하면 사라짐 + // 스크롤 업 하면 보임 + let lastScrollTop = 0; + let lastScrollTime = Date.now(); + const listener = (e: Event) => { + if (e.target) { + const { scrollTop } = e.target as HTMLDivElement; + + const gap = scrollTop - lastScrollTop; + if (gap > 0) { + setIsRefreshButtonVisible(false); + } else if (gap < 0) { + if (!forcePreventScrollRefreshButtonVisible.current) { + setIsRefreshButtonVisible(true); + } + } + + lastScrollTop = scrollTop; + lastScrollTime = Date.now(); + } + }; + scrollElement.addEventListener("scroll", listener); + + const interval = setInterval(() => { + if (lastScrollTop <= 10) { + if (Date.now() - lastScrollTime >= 5000) { + if (!forcePreventScrollRefreshButtonVisible.current) { + setIsRefreshButtonVisible(true); + } else { + lastScrollTime = Date.now(); + } + } + } + }, 1000); + + return () => { + scrollElement.removeEventListener("scroll", listener); + clearInterval(interval); + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const mainHeaderLayoutRef = useRef(null); + + return ( + { + if (isNotReady) { + return; + } + + if (uiConfigStore.showNewSidePanelHeaderTop) { + return { + height: "3rem", + element: ( + { + uiConfigStore.setShowNewSidePanelHeaderTop(false); + + if (mainHeaderLayoutRef.current) { + mainHeaderLayoutRef.current.openSideMenu(); + } + }} + onCloseClick={() => { + uiConfigStore.setShowNewSidePanelHeaderTop(false); + }} + /> + ), + }; + } + })()} + > + {/* side panel에서만 보여준다. 보여주는 로직은 isRefreshButtonVisible를 다루는 useEffect를 참고. refresh button이 로딩중이면 모조건 보여준다. */} + { + setIsRefreshButtonLoading(isLoading); + }} + /> + + + + { + analyticsStore.logEvent("click_main_tab", { + tabName: key, + }); + + setTabStatus(key as TabStatus); + }} + itemMinWidth="5.75rem" + isNotReady={isNotReady} + /> + + { + analyticsStore.logEvent("click_copyAddress"); + setIsOpenDepositModal(true); + }} + isNotReady={isNotReady} + /> + + + + + { + if (!isNotReady) { + animatedPrivacyModeHover.start(isHover ? 1 : 0); + } else { + animatedPrivacyModeHover.set(0); + } + }} + > + + + + + {tabStatus === "available" + ? intl.formatMessage({ + id: "page.main.chart.available", + }) + : intl.formatMessage({ + id: "page.main.chart.staked", + })} + + `${v * 1.25}rem` + ), + }} + > + + Math.max(0, (v - 0.3) * (10 / 3)) + ), + marginTop: "2px", + }} + onClick={(e) => { + e.preventDefault(); + + uiConfigStore.toggleIsPrivacyMode(); + }} + > + {uiConfigStore.isPrivacyMode ? ( + + ) : ( + + )} + + + + + + + +

+ {uiConfigStore.hideStringIfPrivacyMode( + tabStatus === "available" + ? availableTotalPrice?.toString() || "-" + : stakedTotalPrice?.toString() || "-", + 4 + )} +

+
+
+
+
+ {tabStatus === "available" ? ( + { + setIsOpenDepositModal(true); + analyticsStore.logEvent("click_deposit"); + }} + onClickBuy={() => setIsOpenBuy(true)} + isNotReady={isNotReady} + /> + ) : null} + + {tabStatus === "staked" && !isNotReady ? ( + { + e.preventDefault(); + analyticsStore.logEvent("click_keplrDashboard", { + tabName: tabStatus, + }); + + browser.tabs.create({ + url: "https://wallet.keplr.app/?modal=staking&utm_source=keplrextension&utm_medium=button&utm_campaign=permanent&utm_content=manage_stake", + }); + }} + > + + + + + + ) : null} + + + + + {/* + IbcHistoryView 자체가 list를 그리기 때문에 여기서 gutter를 처리하기는 힘들다. + 그러므로 IbcHistoryView에서 gutter를 처리하도록 한다. + */} + + + {tabStatus === "available" && !isNotReady ? ( + + ) : null} + + {!isNotReady ? ( + + {tabStatus === "available" ? ( + { + e.preventDefault(); + + setSearch(e.target.value); + + if (e.target.value.trim().length > 0) { + if (!isEnteredSearch) { + setIsEnteredSearch(true); + } + + const simpleBarScrollRef = + globalSimpleBar.ref.current?.getScrollElement(); + if ( + simpleBarScrollRef && + simpleBarScrollRef.scrollTop < 218 + ) { + searchScrollAnim.start(218, { + from: simpleBarScrollRef.scrollTop, + onChange: (anim: any) => { + // XXX: 이거 실제 파라미터랑 타입스크립트 인터페이스가 다르다...??? + const v = anim.value != null ? anim.value : anim; + if (typeof v === "number") { + simpleBarScrollRef.scrollTop = v; + } + }, + }); + } + } + }} + placeholder={intl.formatMessage({ + id: "page.main.search-placeholder", + })} + /> + ) : null} + + ) : null} + + {/* + AvailableTabView, StakedTabView가 컴포넌트로 빠지면서 밑의 얘들의 각각의 item들에는 stack이 안먹힌다는 걸 주의 + 각 컴포넌트에서 알아서 gutter를 처리해야한다. + */} + {tabStatus === "available" ? ( + { + setIsOpenDepositModal(true); + }} + onMoreTokensClosed={() => { + // token list가 접히면서 scroll height가 작아지게 된다. + // scroll height가 작아지는 것은 위로 스크롤 하는 것과 같은 효과를 내기 때문에 + // 아래와같은 처리가 없으면 token list를 접으면 refesh 버튼이 무조건 나타나게 된다. + // 이게 약간 어색해보이므로 token list를 접을때 1.5초 동안 refresh 버튼 기능을 없애버린다. + forcePreventScrollRefreshButtonVisible.current = true; + setTimeout(() => { + forcePreventScrollRefreshButtonVisible.current = false; + }, 1500); + }} + /> + ) : ( + { + // token list가 접히면서 scroll height가 작아지게 된다. + // scroll height가 작아지는 것은 위로 스크롤 하는 것과 같은 효과를 내기 때문에 + // 아래와같은 처리가 없으면 token list를 접으면 refesh 버튼이 무조건 나타나게 된다. + // 이게 약간 어색해보이므로 token list를 접을때 1.5초 동안 refresh 버튼 기능을 없애버린다. + forcePreventScrollRefreshButtonVisible.current = true; + setTimeout(() => { + forcePreventScrollRefreshButtonVisible.current = false; + }, 1500); + }} + /> + )} + + {tabStatus === "available" && + uiConfigStore.isDeveloper && + !isNotReady ? ( + + ) : null} +
+
+ + setIsOpenDepositModal(false)} + /* Simplebar를 사용하면 트랜지션이 덜덜 떨리는 문제가 있다... */ + forceNotUseSimplebar={true} + > + setIsOpenDepositModal(false)} /> + + + setIsOpenBuy(false)} + > + setIsOpenBuy(false)} + buySupportServiceInfos={buySupportServiceInfos} + /> + + + { + // 꼭 모달 안에서 close 버튼을 눌러야만 닫을 수 있다. + // setIsChangelogModalOpen(false); + }} + onCloseTransitionEnd={() => { + uiConfigStore.changelogConfig.clearLastInfo(); + }} + align="center" + > + { + setIsChangelogModalOpen(false); + }} + updateNotePageData={(() => { + const res: UpdateNotePageData[] = []; + for (const info of uiConfigStore.changelogConfig.showingInfo) { + for (const scene of info.scenes) { + res.push({ + title: scene.title, + image: + scene.image && scene.aspectRatio + ? { + default: scene.image.default, + light: scene.image.light, + aspectRatio: scene.aspectRatio, + } + : undefined, + paragraph: scene.paragraph, + isSidePanelBeta: info.isSidePanelBeta, + }); + } + } + + return res; + })()} + /> + +
+ ); +}); + +const Styles = { + // hover style을 쉽게 넣으려고 그냥 styled-component로 만들었다. + PrivacyModeButton: styled.div` + color: ${(props) => + props.theme.mode === "light" + ? ColorPalette["gray-300"] + : ColorPalette["gray-400"]}; + + &:hover { + color: ${(props) => + props.theme.mode === "light" + ? ColorPalette["gray-200"] + : ColorPalette["gray-300"]}; + } + `, +}; + +const visibleTranslateY = -40; +const invisibleTranslateY = 100; +const RefreshButton: FunctionComponent<{ + visible: boolean; + + onSetIsLoading: (isLoading: boolean) => void; +}> = observer(({ visible, onSetIsLoading }) => { + const { + chainStore, + queriesStore, + starknetQueriesStore, + bitcoinQueriesStore, + accountStore, + priceStore, + } = useStore(); + + const theme = useTheme(); + + const translateY = useSpringValue( + visible ? visibleTranslateY : invisibleTranslateY, + { + config: defaultSpringConfig, + } + ); + useEffect(() => { + translateY.start(visible ? visibleTranslateY : invisibleTranslateY); + }, [translateY, visible]); + + const onSetIsLoadingRef = useRef(onSetIsLoading); + onSetIsLoadingRef.current = onSetIsLoading; + + const [isLoading, _setIsLoading] = useState(false); + const setIsLoading = (isLoading: boolean) => { + _setIsLoading(isLoading); + onSetIsLoadingRef.current(isLoading); + }; + + const refresh = async () => { + if (isLoading) { + return; + } + + setIsLoading(true); + + try { + const promises: Promise[] = []; + + promises.push(priceStore.waitFreshResponse()); + for (const modularChainInfo of chainStore.modularChainInfosInUI) { + const isNeutron = modularChainInfo.chainId === NEUTRON_CHAIN_ID; + + if (isNeutron) { + const account = accountStore.getAccount(modularChainInfo.chainId); + const queries = queriesStore.get(modularChainInfo.chainId); + const queryNeutronRewardInner = + queries.cosmwasm.queryNeutronStakingRewards.getRewardFor( + account.bech32Address + ); + promises.push(queryNeutronRewardInner.waitFreshResponse()); + } else if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain(modularChainInfo.chainId); + const account = accountStore.getAccount(chainInfo.chainId); + + if ( + !chainStore.isEvmChain(chainInfo.chainId) && + account.bech32Address !== "" + ) { + const queries = queriesStore.get(chainInfo.chainId); + const queryBalance = queries.queryBalances.getQueryBech32Address( + account.bech32Address + ); + const queryRewards = + queries.cosmos.queryRewards.getQueryBech32Address( + account.bech32Address + ); + // XXX: 얘는 구조상 waitFreshResponse()가 안되서 일단 쿼리가 끝인지 아닌지는 무시한다. + queryBalance.fetch(); + + promises.push(queryRewards.waitFreshResponse()); + } + + if ( + chainStore.isEvmChain(chainInfo.chainId) && + account.ethereumHexAddress + ) { + const queries = queriesStore.get(chainInfo.chainId); + const queryBalance = + queries.queryBalances.getQueryEthereumHexAddress( + account.ethereumHexAddress + ); + // XXX: 얘는 구조상 waitFreshResponse()가 안되서 일단 쿼리가 끝인지 아닌지는 무시한다. + queryBalance.fetch(); + + for (const currency of chainInfo.currencies) { + const query = queriesStore + .get(chainInfo.chainId) + .queryBalances.getQueryEthereumHexAddress( + account.ethereumHexAddress + ); + + const denomHelper = new DenomHelper(currency.coinMinimalDenom); + if (denomHelper.type === "erc20") { + // XXX: 얘는 구조상 waitFreshResponse()가 안되서 일단 쿼리가 끝인지 아닌지는 무시한다. + query.fetch(); + } + } + } + } else if ("starknet" in modularChainInfo) { + const account = accountStore.getAccount(modularChainInfo.chainId); + + if (account.starknetHexAddress) { + const queries = starknetQueriesStore.get(modularChainInfo.chainId); + + for (const currency of chainStore + .getModularChainInfoImpl(modularChainInfo.chainId) + .getCurrencies("starknet")) { + const query = queries.queryStarknetERC20Balance.getBalance( + modularChainInfo.chainId, + chainStore, + account.starknetHexAddress, + currency.coinMinimalDenom + ); + + if (query) { + // XXX: 얘는 구조상 waitFreshResponse()가 안되서 일단 쿼리가 끝인지 아닌지는 무시한다. + query.fetch(); + } + } + + // refresh starknet staking info + const stakingInfo = queries.stakingInfoManager.getStakingInfo( + account.starknetHexAddress + ); + promises.push(stakingInfo.waitFreshResponse()); + } + } else if ("bitcoin" in modularChainInfo) { + const account = accountStore.getAccount(modularChainInfo.chainId); + const currency = modularChainInfo.bitcoin.currencies[0]; + + if (account.bitcoinAddress) { + const queries = bitcoinQueriesStore.get(modularChainInfo.chainId); + const queryBalance = queries.queryBitcoinBalance.getBalance( + modularChainInfo.chainId, + chainStore, + account.bitcoinAddress.bech32Address, + currency.coinMinimalDenom + ); + + if (queryBalance) { + queryBalance.fetch(); + } + } + } + } + + for (const chainInfo of chainStore.chainInfosInUI) { + const account = accountStore.getAccount(chainInfo.chainId); + const isInitia = chainInfo.chainId === INITIA_CHAIN_ID; + + if (account.bech32Address === "") { + continue; + } + const queries = queriesStore.get(chainInfo.chainId); + const queryUnbonding = isInitia + ? queries.cosmos.queryInitiaUnbondingDelegations.getQueryBech32Address( + account.bech32Address + ) + : queries.cosmos.queryUnbondingDelegations.getQueryBech32Address( + account.bech32Address + ); + const queryDelegation = isInitia + ? queries.cosmos.queryInitiaDelegations.getQueryBech32Address( + account.bech32Address + ) + : queries.cosmos.queryDelegations.getQueryBech32Address( + account.bech32Address + ); + + promises.push(queryUnbonding.waitFreshResponse()); + promises.push(queryDelegation.waitFreshResponse()); + } + + await Promise.all([ + Promise.all(promises), + new Promise((resolve) => setTimeout(resolve, 2000)), + ]); + } catch (e) { + console.log(e); + } finally { + setIsLoading(false); + } + }; + + const rotate = useSpringValue(0, { + config: { + duration: 1250, + easing: easings.linear, + }, + }); + // 밑에서 onRest callback에서 isLoading을 써야하기 때문에 이러한 처리가 필요함. + const isLoadingRef = useRef(isLoading); + isLoadingRef.current = isLoading; + const prevIsLoading = useRef(isLoading); + useEffect(() => { + // 이 코드의 목적은 rotate animation을 실행하는데 + // isLoading이 false가 되었을때 마지막 rotate까지는 끝내도록 하기 위해서 따로 작성된 것임. + if (prevIsLoading.current !== isLoading && isLoading) { + // prev 값과 비교하지 않으면 최초 mount 시점에서 0~360으로 바로 회전하게 된다. + if (isLoading) { + const onRest = () => { + if (isLoadingRef.current) { + rotate.start(360, { + from: 0, + onRest, + }); + } + }; + + rotate.start(360, { + from: 0, + onRest, + }); + } + } + + prevIsLoading.current = isLoading; + }, [rotate, isLoading]); + + return ( + { + e.preventDefault(); + + refresh(); + }} + style={{ + pointerEvents: translateY.to((v) => + // visible이 false일때는 pointer-events를 none으로 해서 클릭을 막는다. + // visibleTranslateY / 2는 대충 정한 값임. 이 값보다 작으면 pointer-events를 none으로 해서 클릭을 막는다. + v >= visibleTranslateY / 2 ? "none" : "auto" + ), + + position: "fixed", + marginBottom: BottomTabsHeightRem, + bottom: 0, + zIndex: 10, + + width: "100%", + maxWidth: SidePanelMaxWidth, + display: "flex", + flexDirection: "column", + alignItems: "center", + + cursor: isLoading ? "progress" : "pointer", + }} + > + `${v}%`), + }} + > + + Refresh + + + `rotate(${v}deg)`), + }} + > + + + + + ); +}); diff --git a/apps/extension/src/pages/main/index.tsx b/apps/extension/src/pages/main/index.tsx index 6642f64ff3..bdd2590c62 100644 --- a/apps/extension/src/pages/main/index.tsx +++ b/apps/extension/src/pages/main/index.tsx @@ -42,7 +42,6 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useGlobarSimpleBar } from "../../hooks/global-simplebar"; import styled, { useTheme } from "styled-components"; import { IbcHistoryView } from "./components/ibc-history-view"; -import { LayeredHorizontalRadioGroup } from "../../components/radio-group"; import { XAxis, YAxis } from "../../components/axis"; import { DepositModal } from "./components/deposit-modal"; import { MainHeaderLayout, MainHeaderLayoutRef } from "./layouts/header"; @@ -435,34 +434,7 @@ export const MainPage: FunctionComponent<{ /> - - { - analyticsStore.logEvent("click_main_tab", { - tabName: key, - }); - - setTabStatus(key as TabStatus); - }} - itemMinWidth="5.75rem" - isNotReady={isNotReady} - /> - + test { analyticsStore.logEvent("click_copyAddress"); From 8969eefba81bf2f2365e843919c06d638d3dd83d Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 16:49:21 +0900 Subject: [PATCH 002/257] [feat] add main-h1 --- apps/extension/src/components/typography/main-h1.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 apps/extension/src/components/typography/main-h1.tsx diff --git a/apps/extension/src/components/typography/main-h1.tsx b/apps/extension/src/components/typography/main-h1.tsx new file mode 100644 index 0000000000..9736fa8d5b --- /dev/null +++ b/apps/extension/src/components/typography/main-h1.tsx @@ -0,0 +1,7 @@ +import styled from "styled-components"; +import { BaseTypography } from "./base"; + +export const MainH1 = styled(BaseTypography)` + font-weight: 500; + font-size: 1.75rem; +`; From b377bf03b87f19215e3e449afd1dcd4cf93e1257 Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 16:49:31 +0900 Subject: [PATCH 003/257] [feat] add lock icon --- apps/extension/src/components/icon/lock.tsx | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/extension/src/components/icon/lock.tsx diff --git a/apps/extension/src/components/icon/lock.tsx b/apps/extension/src/components/icon/lock.tsx new file mode 100644 index 0000000000..c6af8ccbe0 --- /dev/null +++ b/apps/extension/src/components/icon/lock.tsx @@ -0,0 +1,23 @@ +import React, { FunctionComponent } from "react"; +import { IconProps } from "./types"; + +export const LockIcon: FunctionComponent = ({ + width = "1.5rem", + height = "1.5rem", + color, +}) => { + return ( + + + + ); +}; From 9fb363f4ae2479bbaac82a7d8e77089838b853ec Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 16:49:50 +0900 Subject: [PATCH 004/257] [feat] add staked-balance-title texts --- apps/extension/src/languages/en.json | 2 ++ apps/extension/src/languages/ko.json | 2 ++ apps/extension/src/languages/zh-cn.json | 2 ++ 3 files changed, 6 insertions(+) diff --git a/apps/extension/src/languages/en.json b/apps/extension/src/languages/en.json index 90a2646160..1c79df0252 100644 --- a/apps/extension/src/languages/en.json +++ b/apps/extension/src/languages/en.json @@ -636,6 +636,8 @@ "page.main.available.search-empty-view-paragraph": "No Result Found", "page.main.available.search-show-check-manage-asset-view-guide-title": "Can't find a token?", "page.main.available.search-show-check-manage-asset-view-guide-paragraph": "Check if 'Hide Low Balance' is enabled or if it's hidden in 'Manage Asset List'.", + "page.main.balance.staked-balance-title-1": "with", + "page.main.balance.staked-balance-title-2": "staked", "page.main.available.empty-view-title": "Ready to Explore the Interchain?", "page.main.available.empty-view-paragraph": "Gear up yourself by topping up your wallet!", diff --git a/apps/extension/src/languages/ko.json b/apps/extension/src/languages/ko.json index e8198c2da0..e0f874d2b6 100644 --- a/apps/extension/src/languages/ko.json +++ b/apps/extension/src/languages/ko.json @@ -616,6 +616,8 @@ "page.main.available.search-empty-view-paragraph": "검색 결과가 없어요", "page.main.available.search-show-check-manage-asset-view-guide-title": "토큰을 찾을 수 없나요?", "page.main.available.search-show-check-manage-asset-view-guide-paragraph": "소액 숨기기가 활성화되어 있는지 확인하거나 '자산 표시 관리'에서 토큰을 확인해보세요.", + "page.main.balance.staked-balance-title-1": "with", + "page.main.balance.staked-balance-title-2": "Staked", "page.main.available.empty-view-title": "인터체인을 탐험할 준비가 되셨나요?", "page.main.available.empty-view-paragraph": "지갑에 자산을 채워 탐험을 시작해보세요!", diff --git a/apps/extension/src/languages/zh-cn.json b/apps/extension/src/languages/zh-cn.json index 6a46ca1325..5bb4e651df 100644 --- a/apps/extension/src/languages/zh-cn.json +++ b/apps/extension/src/languages/zh-cn.json @@ -578,6 +578,8 @@ "page.main.available.get-started-button": "开始", "page.main.available.new-token-found": "找到{numFoundToken}个新币种", "page.main.available.manage-asset-list-button": "管理资产列表", + "page.main.balance.staked-balance-title-1": "with", + "page.main.balance.staked-balance-title-2": "Staked", "page.main.staked.staked-balance-title": "质押数量", "page.main.staked.staked-balance-tooltip": "质押给验证者并获得收益的币种。", From 06975cf46b596e3c13d406b9c9f735d23eb84158 Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 17:16:38 +0900 Subject: [PATCH 005/257] [feat] remove chart and Tab and add total price --- apps/extension/src/pages/main/index.tsx | 410 +++++++++++------------- 1 file changed, 186 insertions(+), 224 deletions(-) diff --git a/apps/extension/src/pages/main/index.tsx b/apps/extension/src/pages/main/index.tsx index bdd2590c62..b629947c7c 100644 --- a/apps/extension/src/pages/main/index.tsx +++ b/apps/extension/src/pages/main/index.tsx @@ -11,38 +11,30 @@ import { useStore } from "../../stores"; import { Buttons, ClaimAll, - CopyAddress, IBCTransferView, BuyCryptoModal, - StakeWithKeplrDashboardButton, UpdateNoteModal, UpdateNotePageData, } from "./components"; import { Stack } from "../../components/stack"; -import { CoinPretty, PricePretty } from "@keplr-wallet/unit"; -import { - ArrowTopRightOnSquareIcon, - EyeIcon, - EyeSlashIcon, -} from "../../components/icon"; +import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit"; +import { EyeIcon, EyeSlashIcon } from "../../components/icon"; import { Box } from "../../components/box"; import { Modal } from "../../components/modal"; -import { DualChart } from "./components/chart"; import { Gutter } from "../../components/gutter"; -import { H1, Subtitle3, Subtitle4 } from "../../components/typography"; +import { Body2, Subtitle4 } from "../../components/typography"; import { ColorPalette, SidePanelMaxWidth } from "../../styles"; import { AvailableTabView } from "./available"; -import { StakedTabView } from "./staked"; import { SearchTextInput } from "../../components/input"; import { animated, useSpringValue, easings } from "@react-spring/web"; import { defaultSpringConfig } from "../../styles/spring"; import { IChainInfoImpl, QueryError } from "@keplr-wallet/stores"; import { Skeleton } from "../../components/skeleton"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import { useGlobarSimpleBar } from "../../hooks/global-simplebar"; import styled, { useTheme } from "styled-components"; import { IbcHistoryView } from "./components/ibc-history-view"; -import { XAxis, YAxis } from "../../components/axis"; +import { XAxis } from "../../components/axis"; import { DepositModal } from "./components/deposit-modal"; import { MainHeaderLayout, MainHeaderLayoutRef } from "./layouts/header"; import { amountToAmbiguousAverage, isRunningInSidePanel } from "../../utils"; @@ -60,6 +52,8 @@ import { ModularChainInfo } from "@keplr-wallet/types"; import { ChainIdHelper } from "@keplr-wallet/cosmos"; import { AvailableTabLinkButtonList } from "./components/available-tab-link-button-list"; import { INITIA_CHAIN_ID, NEUTRON_CHAIN_ID } from "../../config.ui"; +import { MainH1 } from "../../components/typography/main-h1"; +import { LockIcon } from "../../components/icon/lock"; export interface ViewToken { token: CoinPretty; @@ -77,8 +71,6 @@ export const useIsNotReady = () => { return query.response == null && query.error == null; }; -type TabStatus = "available" | "staked"; - export const MainPage: FunctionComponent<{ setIsNotReady: (isNotReady: boolean) => void; }> = observer(({ setIsNotReady }) => { @@ -100,8 +92,6 @@ export const MainPage: FunctionComponent<{ setIsNotReadyRef.current(isNotReady); }, [isNotReady]); - const [tabStatus, setTabStatus] = React.useState("available"); - const disabledViewAssetTokenMap = uiConfigStore.manageViewAssetTokenConfig.getViewAssetTokenMapByVaultId( keyRingStore.selectedKeyInfo?.id ?? "" @@ -150,18 +140,7 @@ export const MainPage: FunctionComponent<{ } return result; }, [hugeQueriesStore.allKnownBalances, priceStore]); - const availableChartWeight = (() => { - if (!isNotReady && uiConfigStore.isPrivacyMode) { - if (tabStatus === "available") { - return 1; - } - return 0; - } - return availableTotalPrice && !isNotReady - ? Number.parseFloat(availableTotalPrice.toDec().toString()) - : 0; - })(); const stakedTotalPrice = useMemo(() => { let result: PricePretty | undefined; for (const bal of hugeQueriesStore.delegations) { @@ -184,6 +163,7 @@ export const MainPage: FunctionComponent<{ } return result; }, [hugeQueriesStore.delegations, hugeQueriesStore.unbondings]); + const stakedTotalPriceEmbedOnlyUSD = useMemo(() => { let result: PricePretty | undefined; for (const bal of hugeQueriesStore.delegations) { @@ -224,18 +204,30 @@ export const MainPage: FunctionComponent<{ } return result; }, [hugeQueriesStore.delegations, hugeQueriesStore.unbondings, priceStore]); - const stakedChartWeight = (() => { - if (!isNotReady && uiConfigStore.isPrivacyMode) { - if (tabStatus === "staked") { - return 1; - } - return 0; + + const totalPrice = useMemo(() => { + if (!availableTotalPrice) { + return availableTotalPrice; + } + + if (!stakedTotalPrice) { + return stakedTotalPrice; } - return stakedTotalPrice && !isNotReady - ? Number.parseFloat(stakedTotalPrice.toDec().toString()) - : 0; - })(); + return availableTotalPrice.add(stakedTotalPrice); + }, [availableTotalPrice, stakedTotalPrice]); + + const stakedPercentage = useMemo(() => { + if (!totalPrice || !stakedTotalPrice) { + return 0; + } + const totalDec = totalPrice.toDec(); + if (totalDec.isZero()) { + return 0; + } + const stakedDec = stakedTotalPrice.toDec(); + return parseFloat(stakedDec.quo(totalDec).mul(new Dec(100)).toString()); + }, [totalPrice, stakedTotalPrice]); const lastTotalAvailableAmbiguousAvg = useRef(-1); const lastTotalStakedAmbiguousAvg = useRef(-1); @@ -283,7 +275,7 @@ export const MainPage: FunctionComponent<{ const [isEnteredSearch, setIsEnteredSearch] = useState(false); useEffect(() => { // Give focus whenever available tab is selected. - if (!isNotReady && tabStatus === "available") { + if (!isNotReady) { // And clear search text. setSearch(""); @@ -293,7 +285,7 @@ export const MainPage: FunctionComponent<{ }); } } - }, [tabStatus, isNotReady]); + }, [isNotReady]); useEffect(() => { // Log if a search term is entered at least once. if (isEnteredSearch) { @@ -432,27 +424,106 @@ export const MainPage: FunctionComponent<{ setIsRefreshButtonLoading(isLoading); }} /> + + + { + if (!isNotReady) { + animatedPrivacyModeHover.start(isHover ? 1 : 0); + } else { + animatedPrivacyModeHover.set(0); + } + }} + > + + + + {uiConfigStore.hideStringIfPrivacyMode( + totalPrice?.toString().split(".")[0] || "-", + 4 + )} + + {uiConfigStore.hideStringIfPrivacyMode( + totalPrice?.toString().split(".")[1] || "", + 0 + )} + + + `${v * 1.25}rem`), + }} + > + + Math.max(0, (v - 0.3) * (10 / 3)) + ), + marginTop: "2px", + }} + onClick={(e) => { + e.preventDefault(); + uiConfigStore.toggleIsPrivacyMode(); + }} + > + {uiConfigStore.isPrivacyMode ? ( + + ) : ( + + )} + + + + + + + + + + + {intl.formatMessage({ + id: "page.main.balance.staked-balance-title-1", + })} + + + + {`${uiConfigStore.hideStringIfPrivacyMode( + stakedTotalPrice?.toString() || "-", + 4 + )} (${stakedPercentage.toFixed(1)}%) ${intl.formatMessage({ + id: "page.main.balance.staked-balance-title-2", + })}`} + + + + - test + {/* + TODO: 추후 앱 바로 이동 { analyticsStore.logEvent("click_copyAddress"); setIsOpenDepositModal(true); }} isNotReady={isNotReady} - /> + /> */} - - { - if (!isNotReady) { - animatedPrivacyModeHover.start(isHover ? 1 : 0); - } else { - animatedPrivacyModeHover.set(0); - } - }} - > - - - - - {tabStatus === "available" - ? intl.formatMessage({ - id: "page.main.chart.available", - }) - : intl.formatMessage({ - id: "page.main.chart.staked", - })} - - `${v * 1.25}rem` - ), - }} - > - - Math.max(0, (v - 0.3) * (10 / 3)) - ), - marginTop: "2px", - }} - onClick={(e) => { - e.preventDefault(); - - uiConfigStore.toggleIsPrivacyMode(); - }} - > - {uiConfigStore.isPrivacyMode ? ( - - ) : ( - - )} - - - - - - - -

- {uiConfigStore.hideStringIfPrivacyMode( - tabStatus === "available" - ? availableTotalPrice?.toString() || "-" - : stakedTotalPrice?.toString() || "-", - 4 - )} -

-
-
- {tabStatus === "available" ? ( - { - setIsOpenDepositModal(true); - analyticsStore.logEvent("click_deposit"); - }} - onClickBuy={() => setIsOpenBuy(true)} - isNotReady={isNotReady} - /> - ) : null} + { + setIsOpenDepositModal(true); + analyticsStore.logEvent("click_deposit"); + }} + onClickBuy={() => setIsOpenBuy(true)} + isNotReady={isNotReady} + /> - {tabStatus === "staked" && !isNotReady ? ( + {/* {tabStatus === "staked" && !isNotReady ? ( { @@ -586,7 +569,7 @@ export const MainPage: FunctionComponent<{
- ) : null} + ) : null} */} @@ -597,50 +580,46 @@ export const MainPage: FunctionComponent<{ */} - {tabStatus === "available" && !isNotReady ? ( - - ) : null} + {!isNotReady ? : null} {!isNotReady ? ( - {tabStatus === "available" ? ( - { - e.preventDefault(); + { + e.preventDefault(); + + setSearch(e.target.value); - setSearch(e.target.value); - - if (e.target.value.trim().length > 0) { - if (!isEnteredSearch) { - setIsEnteredSearch(true); - } - - const simpleBarScrollRef = - globalSimpleBar.ref.current?.getScrollElement(); - if ( - simpleBarScrollRef && - simpleBarScrollRef.scrollTop < 218 - ) { - searchScrollAnim.start(218, { - from: simpleBarScrollRef.scrollTop, - onChange: (anim: any) => { - // XXX: 이거 실제 파라미터랑 타입스크립트 인터페이스가 다르다...??? - const v = anim.value != null ? anim.value : anim; - if (typeof v === "number") { - simpleBarScrollRef.scrollTop = v; - } - }, - }); - } + if (e.target.value.trim().length > 0) { + if (!isEnteredSearch) { + setIsEnteredSearch(true); } - }} - placeholder={intl.formatMessage({ - id: "page.main.search-placeholder", - })} - /> - ) : null} + + const simpleBarScrollRef = + globalSimpleBar.ref.current?.getScrollElement(); + if ( + simpleBarScrollRef && + simpleBarScrollRef.scrollTop < 218 + ) { + searchScrollAnim.start(218, { + from: simpleBarScrollRef.scrollTop, + onChange: (anim: any) => { + // XXX: 이거 실제 파라미터랑 타입스크립트 인터페이스가 다르다...??? + const v = anim.value != null ? anim.value : anim; + if (typeof v === "number") { + simpleBarScrollRef.scrollTop = v; + } + }, + }); + } + } + }} + placeholder={intl.formatMessage({ + id: "page.main.search-placeholder", + })} + /> ) : null} @@ -648,42 +627,25 @@ export const MainPage: FunctionComponent<{ AvailableTabView, StakedTabView가 컴포넌트로 빠지면서 밑의 얘들의 각각의 item들에는 stack이 안먹힌다는 걸 주의 각 컴포넌트에서 알아서 gutter를 처리해야한다. */} - {tabStatus === "available" ? ( - { - setIsOpenDepositModal(true); - }} - onMoreTokensClosed={() => { - // token list가 접히면서 scroll height가 작아지게 된다. - // scroll height가 작아지는 것은 위로 스크롤 하는 것과 같은 효과를 내기 때문에 - // 아래와같은 처리가 없으면 token list를 접으면 refesh 버튼이 무조건 나타나게 된다. - // 이게 약간 어색해보이므로 token list를 접을때 1.5초 동안 refresh 버튼 기능을 없애버린다. - forcePreventScrollRefreshButtonVisible.current = true; - setTimeout(() => { - forcePreventScrollRefreshButtonVisible.current = false; - }, 1500); - }} - /> - ) : ( - { - // token list가 접히면서 scroll height가 작아지게 된다. - // scroll height가 작아지는 것은 위로 스크롤 하는 것과 같은 효과를 내기 때문에 - // 아래와같은 처리가 없으면 token list를 접으면 refesh 버튼이 무조건 나타나게 된다. - // 이게 약간 어색해보이므로 token list를 접을때 1.5초 동안 refresh 버튼 기능을 없애버린다. - forcePreventScrollRefreshButtonVisible.current = true; - setTimeout(() => { - forcePreventScrollRefreshButtonVisible.current = false; - }, 1500); - }} - /> - )} + { + setIsOpenDepositModal(true); + }} + onMoreTokensClosed={() => { + // token list가 접히면서 scroll height가 작아지게 된다. + // scroll height가 작아지는 것은 위로 스크롤 하는 것과 같은 효과를 내기 때문에 + // 아래와같은 처리가 없으면 token list를 접으면 refesh 버튼이 무조건 나타나게 된다. + // 이게 약간 어색해보이므로 token list를 접을때 1.5초 동안 refresh 버튼 기능을 없애버린다. + forcePreventScrollRefreshButtonVisible.current = true; + setTimeout(() => { + forcePreventScrollRefreshButtonVisible.current = false; + }, 1500); + }} + /> - {tabStatus === "available" && - uiConfigStore.isDeveloper && - !isNotReady ? ( + {uiConfigStore.isDeveloper && !isNotReady ? ( ) : null}
From 0e1d9d9b80fc86f140645f36f43f8464d5fcfe71 Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 21:58:45 +0900 Subject: [PATCH 006/257] [feat] apply skeleton --- apps/extension/src/pages/main/index.tsx | 126 ++++++++++++------------ 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/apps/extension/src/pages/main/index.tsx b/apps/extension/src/pages/main/index.tsx index b629947c7c..32ba340481 100644 --- a/apps/extension/src/pages/main/index.tsx +++ b/apps/extension/src/pages/main/index.tsx @@ -9,7 +9,7 @@ import React, { import { observer } from "mobx-react-lite"; import { useStore } from "../../stores"; import { - Buttons, + // Buttons, ClaimAll, IBCTransferView, BuyCryptoModal, @@ -83,8 +83,10 @@ export const MainPage: FunctionComponent<{ } = useStore(); const isNotReady = useIsNotReady(); + // const isNotReady = true; + const intl = useIntl(); - const theme = useTheme(); + // const theme = useTheme(); const setIsNotReadyRef = useRef(setIsNotReady); setIsNotReadyRef.current = setIsNotReady; @@ -425,9 +427,8 @@ export const MainPage: FunctionComponent<{ }} /> - + { if (!isNotReady) { animatedPrivacyModeHover.start(isHover ? 1 : 0); @@ -436,8 +437,8 @@ export const MainPage: FunctionComponent<{ } }} > - - + + {uiConfigStore.hideStringIfPrivacyMode( totalPrice?.toString().split(".")[0] || "-", @@ -450,66 +451,69 @@ export const MainPage: FunctionComponent<{ )} - + + `${v * 1.25}rem`), + }} + > + `${v * 1.25}rem`), + position: "absolute", + right: 0, + cursor: "pointer", + opacity: animatedPrivacyModeHover.to((v) => + Math.max(0, (v - 0.3) * (10 / 3)) + ), + marginTop: "2px", + }} + onClick={(e) => { + e.preventDefault(); + uiConfigStore.toggleIsPrivacyMode(); }} > - - Math.max(0, (v - 0.3) * (10 / 3)) - ), - marginTop: "2px", - }} - onClick={(e) => { - e.preventDefault(); - uiConfigStore.toggleIsPrivacyMode(); - }} - > - {uiConfigStore.isPrivacyMode ? ( - - ) : ( - - )} - - - - + {uiConfigStore.isPrivacyMode ? ( + + ) : ( + + )} + + + - - - {intl.formatMessage({ - id: "page.main.balance.staked-balance-title-1", - })} - - - - {`${uiConfigStore.hideStringIfPrivacyMode( - stakedTotalPrice?.toString() || "-", - 4 - )} (${stakedPercentage.toFixed(1)}%) ${intl.formatMessage({ - id: "page.main.balance.staked-balance-title-2", - })}`} - - + + + + {intl.formatMessage({ + id: "page.main.balance.staked-balance-title-1", + })} + + + + {`${uiConfigStore.hideStringIfPrivacyMode( + stakedTotalPrice?.toString() || "-", + 4 + )} (${stakedPercentage.toFixed(1)}%) ${intl.formatMessage({ + id: "page.main.balance.staked-balance-title-2", + })}`} + + + @@ -541,14 +545,14 @@ export const MainPage: FunctionComponent<{ - { setIsOpenDepositModal(true); analyticsStore.logEvent("click_deposit"); }} onClickBuy={() => setIsOpenBuy(true)} isNotReady={isNotReady} - /> + /> */} {/* {tabStatus === "staked" && !isNotReady ? ( Date: Thu, 23 Oct 2025 21:59:03 +0900 Subject: [PATCH 007/257] [feat] add icons --- .../src/components/icon/arrow-down-left.tsx | 23 +++++++++ .../src/components/icon/arrow-swap.tsx | 47 +++++++++++++++++++ .../src/components/icon/arrow-up-right.tsx | 23 +++++++++ 3 files changed, 93 insertions(+) create mode 100644 apps/extension/src/components/icon/arrow-down-left.tsx create mode 100644 apps/extension/src/components/icon/arrow-swap.tsx create mode 100644 apps/extension/src/components/icon/arrow-up-right.tsx diff --git a/apps/extension/src/components/icon/arrow-down-left.tsx b/apps/extension/src/components/icon/arrow-down-left.tsx new file mode 100644 index 0000000000..36e98fa3ae --- /dev/null +++ b/apps/extension/src/components/icon/arrow-down-left.tsx @@ -0,0 +1,23 @@ +import React, { FunctionComponent } from "react"; +import { IconProps } from "./types"; + +export const ArrowDownLeftIcon: FunctionComponent = ({ + width = "1rem", + height = "1rem", + color, +}) => { + return ( + + + + ); +}; diff --git a/apps/extension/src/components/icon/arrow-swap.tsx b/apps/extension/src/components/icon/arrow-swap.tsx new file mode 100644 index 0000000000..89f475333e --- /dev/null +++ b/apps/extension/src/components/icon/arrow-swap.tsx @@ -0,0 +1,47 @@ +import React, { FunctionComponent } from "react"; +import { IconProps } from "./types"; + +export const ArrowSwapIcon: FunctionComponent = ({ + width = "1rem", + height = "1rem", + color, +}) => { + return ( + + + + + + + ); +}; diff --git a/apps/extension/src/components/icon/arrow-up-right.tsx b/apps/extension/src/components/icon/arrow-up-right.tsx new file mode 100644 index 0000000000..e2fbda9bc0 --- /dev/null +++ b/apps/extension/src/components/icon/arrow-up-right.tsx @@ -0,0 +1,23 @@ +import React, { FunctionComponent } from "react"; +import { IconProps } from "./types"; + +export const ArrowUpRightIcon: FunctionComponent = ({ + width = "1.5rem", + height = "1.5rem", + color, +}) => { + return ( + + + + ); +}; From cf3752df6aa24e57f84e4990028d050236a67c10 Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 22:00:48 +0900 Subject: [PATCH 008/257] [feat] add text --- apps/extension/src/languages/en.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/extension/src/languages/en.json b/apps/extension/src/languages/en.json index 1c79df0252..b1fb076e83 100644 --- a/apps/extension/src/languages/en.json +++ b/apps/extension/src/languages/en.json @@ -687,6 +687,9 @@ "page.main.components.buttons.deposit-button": "Deposit", "page.main.components.buttons.buy-button": "Buy", "page.main.components.buttons.send-button": "Send", + "page.main.components.buttons.swap-button": "Swap", + "page.main.spendable-card.title": "Spendable", + "page.main.components.claim-all.title": "Claimable Reward", "page.main.components.claim-all.button": "Claim All", "page.main.components.claim-all.claim-button": "Claim", From e4ea5ca406f4dec581c926c6eb47a369a43057c6 Mon Sep 17 00:00:00 2001 From: blacktoast Date: Thu, 23 Oct 2025 22:02:24 +0900 Subject: [PATCH 009/257] [feat] implement SpendableCard --- .../pages/main/components/buttons/index.tsx | 75 ------- .../src/pages/main/components/index.ts | 2 +- .../main/components/spendable-card/index.tsx | 196 ++++++++++++++++++ .../src/pages/main/index-previous.tsx | 22 +- 4 files changed, 208 insertions(+), 87 deletions(-) delete mode 100644 apps/extension/src/pages/main/components/buttons/index.tsx create mode 100644 apps/extension/src/pages/main/components/spendable-card/index.tsx diff --git a/apps/extension/src/pages/main/components/buttons/index.tsx b/apps/extension/src/pages/main/components/buttons/index.tsx deleted file mode 100644 index 9771c32b12..0000000000 --- a/apps/extension/src/pages/main/components/buttons/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { FunctionComponent, useMemo } from "react"; -import { Column, Columns } from "../../../../components/column"; -import { Button } from "../../../../components/button"; -import { Box } from "../../../../components/box"; -import { useNavigate } from "react-router"; -import { observer } from "mobx-react-lite"; -import { useStore } from "../../../../stores"; -import { Dec } from "@keplr-wallet/unit"; -import { Skeleton } from "../../../../components/skeleton"; -import { useIntl } from "react-intl"; - -export const Buttons: FunctionComponent<{ - onClickDeposit: () => void; - onClickBuy: () => void; - isNotReady?: boolean; -}> = observer(({ onClickDeposit, onClickBuy, isNotReady }) => { - const { hugeQueriesStore } = useStore(); - const navigate = useNavigate(); - const intl = useIntl(); - - const balances = hugeQueriesStore.getAllBalances({ - allowIBCToken: true, - }); - const hasBalance = useMemo(() => { - return balances.find((bal) => bal.token.toDec().gt(new Dec(0))) != null; - }, [balances]); - - return ( - - - - -