diff --git a/packages/components/src/content/Breadcrumb/index.tsx b/packages/components/src/content/Breadcrumb/index.tsx index 929491c96d45..e14cabf016a0 100644 --- a/packages/components/src/content/Breadcrumb/index.tsx +++ b/packages/components/src/content/Breadcrumb/index.tsx @@ -40,6 +40,9 @@ const BreadcrumbItem = styled(XStack, { alignItems: 'center', cursor: 'pointer', userSelect: 'none', + borderRadius: '$2', + px: '$2', + py: '$1', pressStyle: { bg: '$bgHover', }, diff --git a/packages/components/src/primitives/Button/index.tsx b/packages/components/src/primitives/Button/index.tsx index 15a6b5d56460..9c0531b090fa 100644 --- a/packages/components/src/primitives/Button/index.tsx +++ b/packages/components/src/primitives/Button/index.tsx @@ -23,7 +23,7 @@ import type { IIconProps, IKeyOfIcons } from '../Icon'; export interface IButtonProps extends ThemeableStackProps { type?: ButtonHTMLAttributes['type']; size?: 'small' | 'medium' | 'large'; - variant?: 'secondary' | 'tertiary' | 'primary' | 'destructive'; + variant?: 'secondary' | 'tertiary' | 'primary' | 'destructive' | 'link'; icon?: IKeyOfIcons; iconAfter?: IKeyOfIcons; disabled?: boolean; @@ -95,6 +95,14 @@ const BUTTON_VARIANTS: Record< activeBg: '$bgStrongActive', focusRingColor: '$focusRing', }, + link: { + color: '$textInfo', + iconColor: '$iconInfo', + bg: '$transparent', + hoverBg: '$transparent', + activeBg: '$transparent', + focusRingColor: '$focusRing', + }, }; export const getSharedButtonStyles = ({ diff --git a/packages/kit-bg/src/services/ServiceStaking.ts b/packages/kit-bg/src/services/ServiceStaking.ts index ec4bbe47aa00..73b421d61fca 100644 --- a/packages/kit-bg/src/services/ServiceStaking.ts +++ b/packages/kit-bg/src/services/ServiceStaking.ts @@ -35,6 +35,7 @@ import type { ECheckAmountActionType, EInternalDappEnum, IAllowanceOverview, + IApyHistoryResponse, IAvailableAsset, IBabylonPortfolioItem, IBuildPermit2ApproveSignDataParams, @@ -45,14 +46,18 @@ import type { IEarnAccountResponse, IEarnAccountToken, IEarnAccountTokenResponse, + IEarnAirdropInvestmentItemV2, IEarnBabylonTrackingItem, IEarnEstimateAction, IEarnEstimateFeeResp, IEarnFAQList, IEarnInvestmentItem, + IEarnInvestmentItemV2, + IEarnManagePageResponse, IEarnPermit2ApproveSignData, IEarnRegisterSignMessageResponse, IEarnSummary, + IEarnSummaryV2, IEarnUnbondingDelegationList, IGetPortfolioParams, IRecommendAsset, @@ -112,6 +117,20 @@ interface IAvailableAssetsResponse { data: { assets: IAvailableAsset[] }; } +interface IAvailableAssetsResponseV2 { + code: string; + message?: string; + data: { + assets: { + type: 'normal' | 'airdrop'; + networkId: string; + provider: string; + symbol: string; + vault?: string; + }[]; + }; +} + @backgroundClass() class ServiceStaking extends ServiceBase { constructor({ backgroundApi }: { backgroundApi: any }) { @@ -163,6 +182,26 @@ class ServiceStaking extends ServiceBase { return response.data.data; } + @backgroundMethod() + async getEarnSummaryV2({ + accountAddress, + networkId, + }: { + accountAddress: string; + networkId: string; + }): Promise { + const client = await this.getClient(EServiceEndpointEnum.Earn); + const response = await client.get<{ + data: IEarnSummaryV2; + }>('/earn/v2/rebate', { + params: { + accountAddress, + networkId, + }, + }); + return response.data.data; + } + @backgroundMethod() public async fetchLocalStakingHistory({ accountId, @@ -623,6 +662,32 @@ class ServiceStaking extends ServiceBase { return result as unknown as IStakeEarnDetail; } + @backgroundMethod() + async getManagePage(params: { + networkId: string; + provider: string; + symbol: string; + vault?: string; + accountAddress: string; + publicKey?: string; + }) { + const client = await this.getClient(EServiceEndpointEnum.Earn); + const requestParams = { + networkId: params.networkId, + provider: params.provider.toLowerCase(), + symbol: params.symbol, + accountAddress: params.accountAddress, + ...(params.vault && { vault: params.vault }), + ...(params.publicKey && { publicKey: params.publicKey }), + }; + + const resp = await client.get<{ data: IEarnManagePageResponse }>( + '/earn/v1/manage-page', + { params: requestParams }, + ); + return resp.data.data; + } + @backgroundMethod() async getTransactionConfirmation(params: { networkId: string; @@ -1028,6 +1093,44 @@ class ServiceStaking extends ServiceBase { return response.data.data; } + @backgroundMethod() + async fetchInvestmentDetailV2(params: { + publicKey?: string | undefined; + vault?: string | undefined; + accountAddress: string; + networkId: string; + provider: string; + symbol: string; + }) { + const client = await this.getClient(EServiceEndpointEnum.Earn); + + const response = await client.get<{ data: IEarnInvestmentItemV2 }>( + `/earn/v2/investment/detail`, + { params }, + ); + + return response.data.data; + } + + @backgroundMethod() + async fetchAirdropInvestmentDetail(params: { + publicKey?: string | undefined; + vault?: string | undefined; + accountAddress: string; + networkId: string; + provider: string; + symbol: string; + }) { + const client = await this.getClient(EServiceEndpointEnum.Earn); + + const response = await client.get<{ data: IEarnAirdropInvestmentItemV2 }>( + `/earn/v1/investment/airdrop-detail`, + { params }, + ); + + return response.data.data; + } + _getAvailableAssets = memoizee( async ({ type }: { type?: EAvailableAssetsTypeEnum }) => { const client = await this.getRawDataClient(EServiceEndpointEnum.Earn); @@ -1062,6 +1165,21 @@ class ServiceStaking extends ServiceBase { void this._getAvailableAssets.clear(); } + @backgroundMethod() + async getAvailableAssetsV2() { + const client = await this.getRawDataClient(EServiceEndpointEnum.Earn); + const resp = await client.get< + IAvailableAssetsResponseV2, + IAxiosResponse + >(`/earn/v2/available-assets`); + + this.handleServerError({ + ...resp.data, + requestId: resp.$requestId, + }); + return resp.data.data.assets; + } + handleServerError(data: { code?: string | number; message?: string; @@ -1322,11 +1440,11 @@ class ServiceStaking extends ServiceBase { } @backgroundMethod() - fetchEarnHomePageData() { - return this._fetchEarnHomePageData(); + fetchEarnHomePageBannerList() { + return this._fetchEarnHomePageBannerList(); } - _fetchEarnHomePageData = memoizee( + _fetchEarnHomePageBannerList = memoizee( async () => { const client = await this.getClient(EServiceEndpointEnum.Utility); const res = await client.get<{ data: IDiscoveryBanner[] }>( @@ -1705,6 +1823,39 @@ class ServiceStaking extends ServiceBase { return null; } } + + @backgroundMethod() + async getApyHistory(params: { + networkId: string; + provider: string; + symbol: string; + vault?: string; + }) { + const client = await this.getClient(EServiceEndpointEnum.Earn); + const requestParams: { + networkId: string; + provider: string; + symbol: string; + vault?: string; + } = { + networkId: params.networkId, + provider: params.provider.toLowerCase(), + symbol: params.symbol, + }; + + if (params.vault) { + requestParams.vault = params.vault; + } + + const response = await client.get( + '/earn/v1/apy/history', + { + params: requestParams, + }, + ); + + return response.data.data; + } } export default ServiceStaking; diff --git a/packages/kit/src/components/LightweightChart/LightweightChart.native.tsx b/packages/kit/src/components/LightweightChart/LightweightChart.native.tsx new file mode 100644 index 000000000000..6237aab5f0ce --- /dev/null +++ b/packages/kit/src/components/LightweightChart/LightweightChart.native.tsx @@ -0,0 +1,99 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { View } from 'react-native'; +import WebView from 'react-native-webview'; + +import { Stack } from '@onekeyhq/components'; + +import { useChartConfig } from './hooks/useChartConfig'; +import { generateChartHTML } from './utils/htmlTemplate'; + +import type { IChartMessage, ILightweightChartProps } from './types'; +import type { WebViewMessageEvent } from 'react-native-webview'; + +export function LightweightChart({ + data, + height, + lineColor, + topColor, + bottomColor, + onHover, +}: ILightweightChartProps) { + const webViewRef = useRef(null); + const [webViewReady, setWebViewReady] = useState(false); + + const chartConfig = useChartConfig({ + data, + lineColor, + topColor, + bottomColor, + }); + const htmlContent = useMemo( + () => generateChartHTML(chartConfig), + [chartConfig], + ); + + const handleMessage = useCallback( + (event: WebViewMessageEvent) => { + try { + const message = JSON.parse(event.nativeEvent.data) as IChartMessage; + + if (message.type === 'ready') { + setWebViewReady(true); + } else if (message.type === 'hover' && onHover) { + onHover({ + time: message.time ? Number(message.time) : undefined, + price: message.price ? Number(message.price) : undefined, + x: message.x, + y: message.y, + }); + } + } catch (error) { + console.error( + 'LightweightChart: Error parsing WebView message:', + error, + ); + } + }, + [onHover], + ); + + // Update chart when data changes + useEffect(() => { + if (webViewReady && webViewRef.current) { + const updateScript = ` + (function() { + const newConfig = ${JSON.stringify(chartConfig)}; + if (window.series) { + window.series.setData(newConfig.data); + window.chart.timeScale().fitContent(); + } + })(); + true; + `; + webViewRef.current.injectJavaScript(updateScript); + } + }, [chartConfig, webViewReady]); + + return ( + + + + + + ); +} diff --git a/packages/kit/src/components/LightweightChart/LightweightChart.tsx b/packages/kit/src/components/LightweightChart/LightweightChart.tsx new file mode 100644 index 000000000000..4b38d49b2e4b --- /dev/null +++ b/packages/kit/src/components/LightweightChart/LightweightChart.tsx @@ -0,0 +1,105 @@ +import { useEffect, useRef } from 'react'; + +import { createChart } from 'lightweight-charts'; + +import { Stack } from '@onekeyhq/components'; + +import { useChartConfig } from './hooks/useChartConfig'; +import { + createAreaSeriesOptions, + createChartOptions, +} from './utils/chartOptions'; + +import type { ILightweightChartProps } from './types'; +import type { IChartApi, ISeriesApi } from 'lightweight-charts'; + +export function LightweightChart({ + data, + height, + lineColor, + topColor, + bottomColor, + onHover, +}: ILightweightChartProps) { + const chartContainerRef = useRef(null); + const chartRef = useRef(null); + const seriesRef = useRef | null>(null); + + const chartConfig = useChartConfig({ + data, + lineColor, + topColor, + bottomColor, + }); + + useEffect(() => { + if (!chartContainerRef.current) return; + + const chart = createChart(chartContainerRef.current, { + ...createChartOptions(chartConfig.theme), + width: chartContainerRef.current.clientWidth, + height, + }); + + const series = chart.addAreaSeries( + createAreaSeriesOptions(chartConfig.theme), + ); + series.setData(chartConfig.data as any); + chart.timeScale().fitContent(); + + chartRef.current = chart; + seriesRef.current = series; + + // Subscribe to crosshair move events + if (onHover) { + chart.subscribeCrosshairMove((param) => { + if ( + param.time && + param.seriesPrices && + param.seriesPrices.size > 0 && + param.point + ) { + const price = param.seriesPrices.get(series); + if (price !== undefined) { + onHover({ + time: + typeof param.time === 'number' + ? param.time + : Number(param.time), + price: typeof price === 'number' ? price : Number(price), + x: param.point.x, + y: param.point.y, + }); + } + } else { + onHover({ + time: undefined, + price: undefined, + x: undefined, + y: undefined, + }); + } + }); + } + + // Handle resize + const resizeObserver = new ResizeObserver((entries) => { + if ( + entries.length === 0 || + entries[0].target !== chartContainerRef.current + ) + return; + const { width: newWidth } = entries[0].contentRect; + chart.applyOptions({ width: newWidth }); + }); + + resizeObserver.observe(chartContainerRef.current); + + return () => { + resizeObserver.disconnect(); + chart.remove(); + }; + }, [chartConfig, height, onHover]); + + return ; +} diff --git a/packages/kit/src/components/LightweightChart/hooks/useChartConfig.ts b/packages/kit/src/components/LightweightChart/hooks/useChartConfig.ts new file mode 100644 index 000000000000..8c95a413ac6b --- /dev/null +++ b/packages/kit/src/components/LightweightChart/hooks/useChartConfig.ts @@ -0,0 +1,47 @@ +import { useMemo } from 'react'; + +import { useTheme } from '@tamagui/core'; + +import type { IMarketTokenChart } from '@onekeyhq/shared/types/market'; + +import { DEFAULT_CHART_COLORS } from '../utils/constants'; + +import type { ILightweightChartConfig } from '../types'; + +interface IUseChartConfigProps { + data: IMarketTokenChart; + lineColor?: string; + topColor?: string; + bottomColor?: string; +} + +export function useChartConfig({ + data, + lineColor = DEFAULT_CHART_COLORS.lineColor, + topColor = DEFAULT_CHART_COLORS.topColor, + bottomColor = DEFAULT_CHART_COLORS.bottomColor, +}: IUseChartConfigProps): ILightweightChartConfig { + const theme = useTheme(); + + return useMemo( + () => ({ + theme: { + bgColor: 'transparent', + textColor: theme.text?.val || '#000000', + textSubduedColor: theme.textSubdued?.val || '#666666', + lineColor, + topColor, + bottomColor, + }, + data: data.map(([time, value]: [number, number]) => ({ time, value })), + }), + [ + data, + theme.text?.val, + theme.textSubdued?.val, + lineColor, + topColor, + bottomColor, + ], + ); +} diff --git a/packages/kit/src/components/LightweightChart/index.ts b/packages/kit/src/components/LightweightChart/index.ts new file mode 100644 index 000000000000..098cd14b3e11 --- /dev/null +++ b/packages/kit/src/components/LightweightChart/index.ts @@ -0,0 +1,3 @@ +export { LightweightChart } from './LightweightChart'; +export type { ILightweightChartProps } from './types'; +export { DEFAULT_CHART_COLORS } from './utils/constants'; diff --git a/packages/kit/src/components/LightweightChart/types/index.ts b/packages/kit/src/components/LightweightChart/types/index.ts new file mode 100644 index 000000000000..b7c84c56e9af --- /dev/null +++ b/packages/kit/src/components/LightweightChart/types/index.ts @@ -0,0 +1,42 @@ +import type { IMarketTokenChart } from '@onekeyhq/shared/types/market'; + +export interface ILightweightChartTheme { + bgColor: string; + textColor: string; + textSubduedColor: string; + lineColor: string; + topColor: string; + bottomColor: string; +} + +export interface ILightweightChartData { + time: number; + value: number; +} + +export interface ILightweightChartConfig { + theme: ILightweightChartTheme; + data: ILightweightChartData[]; +} + +export interface ILightweightChartProps { + data: IMarketTokenChart; + height: number; + lineColor?: string; + topColor?: string; + bottomColor?: string; + onHover?: (data: { + time?: number; + price?: number; + x?: number; + y?: number; + }) => void; +} + +export interface IChartMessage { + type: 'ready' | 'hover'; + time?: string; + price?: string; + x?: number; + y?: number; +} diff --git a/packages/kit/src/components/LightweightChart/utils/chartOptions.ts b/packages/kit/src/components/LightweightChart/utils/chartOptions.ts new file mode 100644 index 000000000000..74bfcaecc694 --- /dev/null +++ b/packages/kit/src/components/LightweightChart/utils/chartOptions.ts @@ -0,0 +1,80 @@ +import type { ILightweightChartTheme } from '../types'; +import type { + AreaSeriesPartialOptions, + ChartOptions, + DeepPartial, +} from 'lightweight-charts'; + +export function createChartOptions( + theme: ILightweightChartTheme, +): DeepPartial { + return { + layout: { + background: { color: theme.bgColor }, + textColor: theme.textSubduedColor, + }, + grid: { + vertLines: { visible: false }, + horzLines: { visible: false }, + }, + crosshair: { + mode: 1, // CrosshairMode.Normal + vertLine: { + color: theme.lineColor, + width: 1, + style: 3, + labelVisible: false, + }, + horzLine: { + visible: false, + }, + }, + timeScale: { + borderVisible: false, + timeVisible: true, + secondsVisible: false, + fixLeftEdge: true, + fixRightEdge: true, + lockVisibleTimeRangeOnResize: true, + }, + rightPriceScale: { + visible: false, + }, + leftPriceScale: { + visible: false, + }, + handleScroll: { + mouseWheel: false, + pressedMouseMove: false, + horzTouchDrag: false, + vertTouchDrag: false, + }, + handleScale: { + axisPressedMouseMove: false, + mouseWheel: false, + pinch: false, + axisDoubleClickReset: false, + }, + kineticScroll: { + touch: false, + mouse: false, + }, + }; +} + +export function createAreaSeriesOptions( + theme: ILightweightChartTheme, +): AreaSeriesPartialOptions { + return { + topColor: theme.topColor, + bottomColor: theme.bottomColor, + lineColor: theme.lineColor, + lineWidth: 3 as any, + lastValueVisible: false, + priceLineVisible: false, + priceFormat: { + type: 'custom', + formatter: (price: number) => `${price.toFixed(2)}%`, + }, + }; +} diff --git a/packages/kit/src/components/LightweightChart/utils/constants.ts b/packages/kit/src/components/LightweightChart/utils/constants.ts new file mode 100644 index 000000000000..d6d70c09f922 --- /dev/null +++ b/packages/kit/src/components/LightweightChart/utils/constants.ts @@ -0,0 +1,10 @@ +// Use the same version as defined in package.json +export const LIGHTWEIGHT_CHARTS_VERSION = '3.8.0'; + +export const LIGHTWEIGHT_CHARTS_CDN = `https://unpkg.com/lightweight-charts@${LIGHTWEIGHT_CHARTS_VERSION}/dist/lightweight-charts.standalone.production.js`; + +export const DEFAULT_CHART_COLORS = { + lineColor: '#33C641', + topColor: '#00B81233', + bottomColor: '#00FF1900', +}; diff --git a/packages/kit/src/components/LightweightChart/utils/htmlTemplate.ts b/packages/kit/src/components/LightweightChart/utils/htmlTemplate.ts new file mode 100644 index 000000000000..5211cbbb6744 --- /dev/null +++ b/packages/kit/src/components/LightweightChart/utils/htmlTemplate.ts @@ -0,0 +1,145 @@ +import { LIGHTWEIGHT_CHARTS_CDN } from './constants'; + +import type { ILightweightChartConfig } from '../types'; + +function getStyles(): string { + return ` + * { margin: 0; padding: 0; box-sizing: border-box; } + html, body { width: 100%; height: 100%; overflow: hidden; } + #chart { width: 100%; height: 100%; } + .tv-lightweight-charts table tr:last-child { pointer-events: none !important; } + `.trim(); +} + +function getChartInitScript(): string { + return ` + const chart = LightweightCharts.createChart(container, { + layout: { + background: { color: config.theme.bgColor }, + textColor: config.theme.textSubduedColor, + }, + grid: { + vertLines: { visible: false }, + horzLines: { visible: false }, + }, + crosshair: { + mode: LightweightCharts.CrosshairMode.Normal, + vertLine: { + color: config.theme.lineColor, + width: 1, + style: 3, + labelVisible: false, + }, + horzLine: { visible: false }, + }, + timeScale: { + borderVisible: false, + timeVisible: true, + secondsVisible: false, + fixLeftEdge: true, + fixRightEdge: true, + lockVisibleTimeRangeOnResize: true, + tickMarkFormatter: (time) => { + const date = new Date(time * 1000); + const month = date.toLocaleDateString('en-US', { month: 'short' }); + const day = date.getDate().toString().padStart(2, '0'); + return month + ' ' + day; + }, + }, + rightPriceScale: { visible: false }, + leftPriceScale: { visible: false }, + handleScroll: { + mouseWheel: false, + pressedMouseMove: false, + horzTouchDrag: false, + vertTouchDrag: false, + }, + handleScale: { + axisPressedMouseMove: false, + mouseWheel: false, + pinch: false, + axisDoubleClickReset: false, + }, + kineticScroll: { + touch: false, + mouse: false, + }, + }); + + const series = chart.addAreaSeries({ + topColor: config.theme.topColor, + bottomColor: config.theme.bottomColor, + lineColor: config.theme.lineColor, + lineWidth: 2.5, + lastValueVisible: false, + priceLineVisible: false, + priceFormat: { + type: 'custom', + formatter: (price) => price.toFixed(2) + '%', + }, + }); + + series.setData(config.data); + chart.timeScale().fitContent(); + `.trim(); +} + +function getEventHandlers(): string { + return ` + chart.subscribeCrosshairMove((param) => { + const message = param.time && param.seriesPrices?.size > 0 && param.point + ? { + type: 'hover', + time: String(param.time), + price: String(param.seriesPrices.get(series)), + x: param.point.x, + y: param.point.y, + } + : { type: 'hover', time: undefined, price: undefined, x: undefined, y: undefined }; + + window.ReactNativeWebView.postMessage(JSON.stringify(message)); + }); + + new ResizeObserver(entries => { + if (entries.length) { + const { width, height } = entries[0].contentRect; + chart.applyOptions({ width, height }); + } + }).observe(container); + + window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ready' })); + `.trim(); +} + +function getChartScript(config: ILightweightChartConfig): string { + const configJSON = JSON.stringify(config); + + return ` + (function() { + const config = ${configJSON}; + const container = document.getElementById('chart'); + + ${getChartInitScript()} + ${getEventHandlers()} + })(); + `.trim(); +} + +/** + * Generates HTML template for LightweightChart WebView + * This is a self-contained HTML page that renders a chart using lightweight-charts library + */ +export function generateChartHTML(config: ILightweightChartConfig): string { + return ` + + + + + + + +
+ + +`; +} diff --git a/packages/kit/src/components/ListView/TableList.tsx b/packages/kit/src/components/ListView/TableList.tsx new file mode 100644 index 000000000000..4e0a9ebee023 --- /dev/null +++ b/packages/kit/src/components/ListView/TableList.tsx @@ -0,0 +1,755 @@ +/** + * TableList Component + * + * A flexible table component with support for: + * - Responsive column hiding based on priority + * - Expandable rows with custom content + * - Sorting, actions, and custom rendering + * + * @example Priority-based responsive hiding + * ```tsx + *
- + + + {displayHomePage ? ( useMemo(() => getNetworkIdsMap().onekeyall, []); -const getNumberColor = ( - value: string | number, - defaultColor: ISizableTextProps['color'] = '$textSuccess', -): ISizableTextProps['color'] => - (typeof value === 'string' ? Number(value) : value) === 0 - ? '$text' - : defaultColor; - -const toTokenProviderListPage = async ( - navigation: ReturnType, - { - networkId, - accountId, - indexedAccountId, - symbol, - protocols, - }: { - networkId: string; - accountId: string; - indexedAccountId?: string; - symbol: string; - protocols: IEarnAvailableAssetProtocol[]; - }, -) => { - defaultLogger.staking.page.selectAsset({ tokenSymbol: symbol }); - const earnAccount = await backgroundApiProxy.serviceStaking.getEarnAccount({ - accountId, - indexedAccountId, - networkId, - }); - - if (protocols.length === 1) { - const protocol = protocols[0]; - navigation.pushModal(EModalRoutes.StakingModal, { - screen: EModalStakingRoutes.ProtocolDetailsV2, - params: { - networkId: protocol.networkId, - accountId: earnAccount?.accountId || accountId, - indexedAccountId: - earnAccount?.account.indexedAccountId || indexedAccountId, - symbol, - provider: protocol.provider, - vault: protocol.vault, - }, - }); - return; - } - - // Show dialog for multiple protocols instead of navigating to modal - showProtocolListDialog({ - symbol, - accountId: earnAccount?.accountId || accountId, - indexedAccountId: earnAccount?.account.indexedAccountId || indexedAccountId, - onProtocolSelect: async (params) => { - navigation.pushModal(EModalRoutes.StakingModal, { - screen: EModalStakingRoutes.ProtocolDetailsV2, - params, - }); - }, - }); -}; - -function RecommendedSkeletonItem({ ...rest }: IYStackProps) { - return ( - - - - - - - - - - - - ); -} - -function RecommendedItem({ - token, - ...rest -}: { token?: IRecommendAsset } & IYStackProps) { - const accountInfo = useActiveAccount({ num: 0 }); - const navigation = useAppNavigation(); - const { - activeAccount: { account, indexedAccount }, - } = accountInfo; - - const noWalletConnected = useMemo( - () => !account && !indexedAccount, - [account, indexedAccount], - ); - - const onPress = useCallback(async () => { - if (token) { - const earnAccount = - await backgroundApiProxy.serviceStaking.getEarnAccount({ - indexedAccountId: indexedAccount?.id, - accountId: account?.id ?? '', - networkId: token.protocols[0]?.networkId, - }); - await toTokenProviderListPage(navigation, { - indexedAccountId: - earnAccount?.account.indexedAccountId || indexedAccount?.id, - accountId: earnAccount?.accountId || account?.id || '', - networkId: token.protocols[0]?.networkId, - symbol: token.symbol, - protocols: token.protocols, - }); - } - }, [account?.id, indexedAccount?.id, navigation, token]); - - if (!token) { - return ; - } - - return ( - - - - - - - - } - /> - - {token.symbol} - - - - - - {!noWalletConnected ? ( - - {token?.available?.text} - - ) : null} - - - - ); -} - -function RecommendedContainer({ children }: PropsWithChildren) { - const intl = useIntl(); - return ( - - {/* since the children have been used negative margin, so we should use zIndex to make sure the trigger of popover is on top of the children */} - - - {intl.formatMessage({ id: ETranslations.market_trending })} - - - {children} - - ); -} - -function Recommended() { - const { md } = useMedia(); - const allNetworkId = useAllNetworkId(); - const { - activeAccount: { account, indexedAccount }, - } = useActiveAccount({ num: 0 }); - const [{ refreshTrigger = 0 }] = useEarnAtom(); - - const { result: tokens } = usePromiseResult( - async () => { - const recommendedAssets = - await backgroundApiProxy.serviceStaking.fetchAllNetworkAssetsV2({ - accountId: account?.id ?? '', - networkId: allNetworkId, - indexedAccountId: account?.indexedAccountId || indexedAccount?.id, - }); - return recommendedAssets?.tokens || []; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - account?.id, - allNetworkId, - account?.indexedAccountId, - indexedAccount?.id, - refreshTrigger, - ], - { - watchLoading: true, - initResult: [], - }, - ); - - // Render skeleton when loading and no data - const shouldShowSkeleton = tokens.length === 0; - if (shouldShowSkeleton) { - return ( - - {/* Desktop/Extension with larger screen: 4 items per row */} - {platformEnv.isNative ? ( - // Mobile: horizontal scrolling skeleton - - - {Array.from({ length: 4 }).map((_, index) => ( - - - - ))} - - - ) : ( - // Desktop/Extension: grid layout - - {Array.from({ length: 4 }).map((_, index) => ( - - - - ))} - - )} - - ); - } - - // Render actual tokens - if (tokens.length) { - return ( - - {platformEnv.isNative ? ( - // Mobile: horizontal scrolling - - - {tokens.map((token) => ( - - - - ))} - - - ) : ( - // Desktop/Extension: grid layout - - {tokens.map((token) => ( - - - - ))} - - )} - - ); - } - return null; -} - -function Overview({ - isLoading, - onRefresh, -}: { - isLoading: boolean; - onRefresh: () => void; -}) { - const { - activeAccount: { account, indexedAccount }, - } = useActiveAccount({ num: 0 }); - const actions = useEarnActions(); - const allNetworkId = useAllNetworkId(); - const totalFiatMapKey = useMemo( - () => - actions.current.buildEarnAccountsKey({ - accountId: account?.id, - indexAccountId: indexedAccount?.id, - networkId: allNetworkId, - }), - [account?.id, actions, allNetworkId, indexedAccount?.id], - ); - const [{ earnAccount }] = useEarnAtom(); - const [settings] = useSettingsPersistAtom(); - const totalFiatValue = useMemo( - () => earnAccount?.[totalFiatMapKey]?.totalFiatValue || '0', - [earnAccount, totalFiatMapKey], - ); - const earnings24h = useMemo( - () => earnAccount?.[totalFiatMapKey]?.earnings24h || '0', - [earnAccount, totalFiatMapKey], - ); - const hasClaimableAssets = useMemo( - () => earnAccount?.[totalFiatMapKey]?.hasClaimableAssets || false, - [earnAccount, totalFiatMapKey], - ); - const isOverviewLoaded = useMemo( - () => earnAccount?.[totalFiatMapKey]?.isOverviewLoaded || false, - [earnAccount, totalFiatMapKey], - ); - const navigation = useAppNavigation(); - const onPress = useCallback(() => { - navigation.pushModal(EModalRoutes.StakingModal, { - screen: EModalStakingRoutes.InvestmentDetails, - }); - }, [navigation]); - const intl = useIntl(); - - const handleRefresh = useCallback(() => { - onRefresh(); - void backgroundApiProxy.serviceStaking.clearAvailableAssetsCache(); - actions.current.triggerRefresh(); - }, [onRefresh, actions]); - - return ( - - {/* total value */} - - - {intl.formatMessage({ id: ETranslations.earn_total_staked_value })} - - - - {totalFiatValue} - - {platformEnv.isNative ? null : ( - - )} - - - {/* 24h earnings */} - - - {earnings24h} - - - - {intl.formatMessage({ id: ETranslations.earn_24h_earnings })} - - - } - title={intl.formatMessage({ - id: ETranslations.earn_24h_earnings, - })} - renderContent={ - - {intl.formatMessage({ - id: ETranslations.earn_24h_earnings_tooltip, - })} - - } - /> - - - - {/* details button */} - {!isOverviewLoaded ? null : ( - - )} - - ); -} - -function EarnBlockedOverview(props: { - showHeader?: boolean; - showContent?: boolean; - icon: IKeyOfIcons; - title: string; - description: string; - refresh: () => Promise; - refreshing: boolean; -}) { - const intl = useIntl(); - const { - title, - description, - icon, - refresh, - refreshing, - showHeader, - showContent, - } = props; - - return ( - - {showHeader ? ( - - ) : null} - - - {intl.formatMessage({ - id: ETranslations.global_refresh, - })} - - } - /> - - - ); -} - -function EarnHomeContent({ +function BasicEarnHome({ showHeader, showContent, + overrideDefaultTab, }: { showHeader?: boolean; showContent?: boolean; + overrideDefaultTab?: 'assets' | 'portfolio' | 'faqs'; }) { + const route = useAppRoute(); const { activeAccount } = useActiveAccount({ num: 0 }); const { account, indexedAccount } = activeAccount; const media = useMedia(); const actions = useEarnActions(); - const allNetworkId = useAllNetworkId(); - const { - isLoading: isFetchingBlockResult, - run: refreshBlockResult, - result: blockResult, - } = usePromiseResult( - async () => { - const blockData = - await backgroundApiProxy.serviceStaking.getBlockRegion(); - return { blockData }; - }, - [], - { - revalidateOnFocus: true, - }, - ); + const { isFetchingBlockResult, refreshBlockResult, blockResult } = + useBlockRegion(); - const isAccountExists = !!account; - - const { isLoading: isFetchingAccounts, run: refreshOverViewData } = - usePromiseResult( - async () => { - if (!isAccountExists && !indexedAccount?.id) { - return; - } - const totalFiatMapKey = actions.current.buildEarnAccountsKey({ - accountId: account?.id, - indexAccountId: indexedAccount?.id, - networkId: allNetworkId, - }); - - const fetchAndUpdateOverview = async () => { - if (!isAccountExists && !indexedAccount?.id) { - return; - } + const { isFetchingAccounts, refreshEarnAccounts } = useEarnAccounts(); - const overviewData = - await backgroundApiProxy.serviceStaking.fetchAccountOverview({ - accountId: account?.id ?? '', - networkId: allNetworkId, - indexedAccountId: account?.indexedAccountId || indexedAccount?.id, - }); - const earnAccountData = - actions.current.getEarnAccount(totalFiatMapKey); - actions.current.updateEarnAccounts({ - key: totalFiatMapKey, - earnAccount: { - accounts: earnAccountData?.accounts || [], - ...overviewData, - isOverviewLoaded: true, - }, - }); - }; + const { earnBanners, refetchBanners } = useBannerInfo(); + const { faqList, isFaqLoading, refetchFAQ } = useFAQListInfo(); - const earnAccountData = actions.current.getEarnAccount(totalFiatMapKey); - if (earnAccountData) { - await timerUtils.wait(350); - await fetchAndUpdateOverview(); - } else { - await fetchAndUpdateOverview(); - } - return { loaded: true }; - }, - [ - account?.id, - account?.indexedAccountId, - actions, - allNetworkId, - indexedAccount?.id, - isAccountExists, - ], - { - watchLoading: true, - pollingInterval: timerUtils.getTimeDurationMs({ minute: 3 }), - revalidateOnReconnect: true, - alwaysSetState: true, - }, - ); + const navigation = useAppNavigation(); - const { result: earnBanners, run: refetchBanners } = usePromiseResult( - async () => { - const bannerResult = - await backgroundApiProxy.serviceStaking.fetchEarnHomePageData(); - return ( - bannerResult?.map((i) => ({ - ...i, - imgUrl: i.src, - title: i.title || '', - titleTextProps: { - size: '$headingMd', - }, - })) || [] - ); - }, - [], - { - revalidateOnReconnect: true, - revalidateOnFocus: true, - }, - ); + // Get tab from route params or override (for Discovery tab embedding) + const defaultTab = overrideDefaultTab || route.params?.tab; - const { - result: faqList, - isLoading: isFaqLoading, - run: refetchFAQ, - } = usePromiseResult( - async () => { - const result = - await backgroundApiProxy.serviceStaking.getFAQListForHome(); - return result; - }, - [], - { - initResult: [], - watchLoading: true, - revalidateOnFocus: true, + // Handle tab change - update route params + const handleTabChange = useCallback( + (tab: 'assets' | 'portfolio' | 'faqs') => { + navigation.navigate(ETabEarnRoutes.EarnHome, { tab }); }, + [navigation], ); - const navigation = useAppNavigation(); - const accountSelectorActions = useAccountSelectorActions(); // Listen to tab focus state and refetch incomplete data @@ -862,35 +128,8 @@ function EarnHomeContent({ ), ); - // Create adapter function for AvailableAssetsTabViewList - const handleTokenPress = useCallback( - async (params: { - networkId: string; - accountId: string; - indexedAccountId?: string; - symbol: string; - protocols: IEarnAvailableAssetProtocol[]; - }) => { - await toTokenProviderListPage(navigation, params); - }, - [navigation], - ); - const onBannerPress = useCallback( - async ({ - hrefType, - href, - }: { - imgUrl: string; - title: string; - bannerId: string; - src: string; - href: string; - hrefType: string; - rank: number; - useSystemBrowser: boolean; - theme?: 'light' | 'dark'; - }) => { + async ({ hrefType, href }: IDiscoveryBanner) => { if (account || indexedAccount) { if (href.includes('/defi/staking')) { const [path, query] = href.split('?'); @@ -949,96 +188,18 @@ function EarnHomeContent({ [account, accountSelectorActions, indexedAccount, navigation], ); - const banners = useMemo(() => { - // Only show skeleton if earnBanners is undefined - const shouldShowSkeleton = earnBanners === undefined; - - if (earnBanners) { - return earnBanners.length ? ( - - ) : null; - } - - if (shouldShowSkeleton) { - return ( - - ); - } - - return null; - }, [earnBanners, media.gtLg, onBannerPress]); + const banners = useMemo( + () => ( + + + + ), + [earnBanners, onBannerPress], + ); const isLoading = !!isFetchingAccounts; - - const faqPanel = useMemo(() => { - // Only show loading if we have no data - const shouldShowLoading = - isFaqLoading && (!faqList || faqList.length === 0); - return ; - }, [faqList, isFaqLoading]); - - const gtLgFaqPanel = useMemo(() => { - return media.gtLg && (isFaqLoading || faqList.length > 0) ? ( - - {faqPanel} - - ) : null; - }, [media.gtLg, isFaqLoading, faqList.length, faqPanel]); const intl = useIntl(); - const tabData = useMemo( - () => [ - { - title: intl.formatMessage({ id: ETranslations.global_all }), - type: EAvailableAssetsTypeEnum.All, - }, - { - // eslint-disable-next-line spellcheck/spell-checker - title: intl.formatMessage({ id: ETranslations.earn_stablecoins }), - type: EAvailableAssetsTypeEnum.StableCoins, - }, - { - title: intl.formatMessage({ id: ETranslations.earn_native_tokens }), - type: EAvailableAssetsTypeEnum.NativeTokens, - }, - ], - [intl], - ); - const [tabPageHeight, setTabPageHeight] = useState( platformEnv.isNativeIOS ? 143 : 92, ); @@ -1065,110 +226,47 @@ function EarnHomeContent({ if (platformEnv.isNative && media.md) { return ( <> - {showHeader ? : null} - ( - - {/* overview and banner */} - - - {banners ? ( - - {banners} + {showHeader && showContent ? : null} + ( + + + + - ) : null} - - {/* Recommended, available assets and introduction */} - - - + {banners ? ( + + {banners} + + ) : null} - {/* FAQ Panel */} - {banners ? gtLgFaqPanel : null} - - {intl.formatMessage({ - id: ETranslations.earn_available_assets, - })} - - - )} - renderTabBar={(props) => ( - ( - onPress(name)} - > - - {name} - - - )} - /> - )} - > - {tabData.map((item) => ( - - - } - > - - - - ))} - - {showHeader && platformEnv.isNative ? ( + ), + }} + /> + {showHeader && showContent && platformEnv.isNative ? ( - - - } - > - {/* container */} - - - {/* overview and banner */} - - - {banners ? ( - - {banners} - - ) : null} - - {/* Recommended, available assets and introduction */} - - - - - - {/* FAQ Panel */} - {banners ? gtLgFaqPanel : null} - - {banners && - (media.gtLg || (faqList.length === 0 && !isFaqLoading)) ? null : ( - - {faqPanel} - - )} + + } + > + + {/* overview and banner */} + + + + + {banners ? ( + + {banners} - {media.gtLg && !banners ? ( - {gtLgFaqPanel} - ) : null} - - - - + ) : null} + + + + ); } export function EarnHomeWithProvider({ showHeader = true, showContent = true, + defaultTab, }: { showHeader?: boolean; showContent?: boolean; + defaultTab?: 'assets' | 'portfolio' | 'faqs'; }) { return ( - + ); @@ -1304,6 +362,9 @@ const useNavigateToNativeEarnPage = platformEnv.isNative ? () => { const { md } = useMedia(); const navigation = useAppNavigation(); + const route = useAppRoute(); + const tabParam = route.params?.tab; + useLayoutEffect(() => { if (md) { navigation.navigate( @@ -1312,6 +373,7 @@ const useNavigateToNativeEarnPage = platformEnv.isNative screen: ETabDiscoveryRoutes.TabDiscovery, params: { defaultTab: ETranslations.global_earn, + earnTab: tabParam, // Pass the tab parameter }, }, { @@ -1319,17 +381,19 @@ const useNavigateToNativeEarnPage = platformEnv.isNative }, ); } - }, [navigation, md]); + }, [navigation, md, tabParam]); } : () => {}; export default function EarnHome() { useNavigateToNativeEarnPage(); - return ( + return platformEnv.isNative ? ( + ) : ( + ); } diff --git a/packages/kit/src/views/Earn/components/AvailableAssetsTabViewList.tsx b/packages/kit/src/views/Earn/components/AvailableAssetsTabViewList.tsx index 42c712cb3f7a..85877caa06a7 100644 --- a/packages/kit/src/views/Earn/components/AvailableAssetsTabViewList.tsx +++ b/packages/kit/src/views/Earn/components/AvailableAssetsTabViewList.tsx @@ -17,6 +17,10 @@ import { } from '@onekeyhq/components'; import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; import { ListItem } from '@onekeyhq/kit/src/components/ListItem'; +import { TableList } from '@onekeyhq/kit/src/components/ListView/TableList'; +import type { ITableColumn } from '@onekeyhq/kit/src/components/ListView/TableList'; +import { NetworkAvatarGroup } from '@onekeyhq/kit/src/components/NetworkAvatar/NetworkAvatar'; +import { Token } from '@onekeyhq/kit/src/components/Token'; import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accountSelector'; import { @@ -24,94 +28,23 @@ import { useEarnAtom, } from '@onekeyhq/kit/src/states/jotai/contexts/earn'; import { ETranslations } from '@onekeyhq/shared/src/locale'; -import type { IEarnAvailableAssetProtocol } from '@onekeyhq/shared/types/earn'; +import type { IEarnAvailableAsset } from '@onekeyhq/shared/types/earn'; import { EAvailableAssetsTypeEnum } from '@onekeyhq/shared/types/earn'; -import { AprText } from './AprText'; -import { FAQPanel } from './FAQPanel'; - -// Skeleton component for loading state -function AvailableAssetsSkeleton() { - const media = useMedia(); - - return ( - - {Array.from({ length: 4 }).map((_, index) => ( - - - - - - - - - - {media.gtLg ? ( - - ) : null} - - - ))} - - ); -} +import { useToTokenProviderListPage } from '../hooks/useToTokenProviderListPage'; -interface IAvailableAssetsTabViewListProps { - onTokenPress?: (params: { - networkId: string; - accountId: string; - indexedAccountId?: string; - symbol: string; - protocols: IEarnAvailableAssetProtocol[]; - }) => Promise; -} +import { AprText } from './AprText'; -export function AvailableAssetsTabViewList({ - onTokenPress, -}: IAvailableAssetsTabViewListProps) { +export function AvailableAssetsTabViewList() { const { activeAccount: { account, indexedAccount }, } = useActiveAccount({ num: 0 }); const [{ availableAssetsByType = {}, refreshTrigger = 0 }] = useEarnAtom(); const actions = useEarnActions(); const intl = useIntl(); - const media = useMedia(); const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const media = useMedia(); + const toTokenProviderListPage = useToTokenProviderListPage(); const tabData = useMemo( () => [ @@ -195,320 +128,172 @@ export function AvailableAssetsTabViewList({ [focusedTab, tabData], ); - if (assets.length || isLoading) { - return ( - - - {intl.formatMessage({ id: ETranslations.earn_available_assets })} - - ( - onPress(name)} - > - - {name} - - - )} - /> - - {isLoading && assets.length === 0 ? ( - - ) : ( - - {assets.map((asset, index) => { - const { name, logoURI, symbol, badges = [], protocols } = asset; - - return ( - [] = useMemo( + () => [ + { + key: 'asset', + label: intl.formatMessage({ id: ETranslations.global_asset }), + flex: 2, + sortable: true, + comparator: (a, b) => a.symbol.localeCompare(b.symbol), + render: (asset) => ( + + + {asset.symbol} + + {asset.badges?.map((badge) => ( + { - await onTokenPress?.({ - networkId: protocols[0]?.networkId || '', - accountId: account?.id ?? '', - indexedAccountId: indexedAccount?.id, - symbol, - protocols, - }); - }} - avatarProps={{ - src: logoURI, - fallbackProps: { - borderRadius: '$full', - }, - ...(media.gtLg - ? { - size: '$8', - } - : {}), - }} - {...(media.gtLg - ? { - drillIn: true, - mx: '$0', - px: '$4', - borderRadius: '$0', - } - : {})} - {...(index !== 0 && media.gtLg - ? { - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: '$borderSubdued', - } - : {})} > - - {symbol} - - {badges.map((badge) => ( - - {badge.tag} - - ))} - - - } - /> - - - - - - - ); - })} - - )} - - ); - } - return null; -} - -export function AvailableAssetsTabViewListMobile({ - onTokenPress, - assetType, - faqList, -}: IAvailableAssetsTabViewListProps & { - assetType: EAvailableAssetsTypeEnum; - faqList?: Array<{ question: string; answer: string }>; -}) { - const { - activeAccount: { account, indexedAccount }, - } = useActiveAccount({ num: 0 }); - const [{ availableAssetsByType = {}, refreshTrigger = 0 }] = useEarnAtom(); - const actions = useEarnActions(); - const media = useMedia(); - - // Get filtered assets based on selected tab - const assets = useMemo(() => { - return availableAssetsByType[assetType] || []; - }, [assetType, availableAssetsByType]); - - // Throttled function to fetch assets data - const fetchAssetsData = useThrottledCallback( - async (tabType: EAvailableAssetsTypeEnum) => { - const loadingKey = `availableAssets-${tabType}`; - actions.current.setLoadingState(loadingKey, true); - - try { - const tabAssets = - await backgroundApiProxy.serviceStaking.getAvailableAssets({ - type: tabType, - }); + {badge.tag} + + ))} + + + ), + }, + { + key: 'network', + label: intl.formatMessage({ id: ETranslations.global_network }), + flex: 1, + hideInMobile: true, + render: (asset) => ( + p.networkId)), + )} + size="$5" + variant="spread" + maxVisible={3} + /> + ), + }, + { + key: 'yield', + label: intl.formatMessage({ id: ETranslations.global_apr }), + flex: 1, + align: 'flex-end', + sortable: true, + comparator: (a, b) => { + const aprA = parseFloat(a.aprWithoutFee || a.apr || '0'); + const aprB = parseFloat(b.aprWithoutFee || b.apr || '0'); + return aprA - aprB; + }, + render: (asset) => , + }, + ], + [intl], + ); - // Update the corresponding data in atom - actions.current.updateAvailableAssetsByType(tabType, tabAssets); - return tabAssets; - } finally { - actions.current.setLoadingState(loadingKey, false); - } + // Handle row press + const handleRowPress = useCallback( + async (asset: IEarnAvailableAsset) => { + await toTokenProviderListPage({ + networkId: asset.protocols[0]?.networkId || '', + accountId: account?.id ?? '', + indexedAccountId: indexedAccount?.id, + symbol: asset.symbol, + protocols: asset.protocols, + logoURI: asset.logoURI, + }); }, - 200, - { leading: true, trailing: false }, + [account?.id, indexedAccount?.id, toTokenProviderListPage], ); - // Load data for the selected tab - const { isLoading } = usePromiseResult( - async () => { - if (assetType) { - const result = await fetchAssetsData(assetType); - return result || []; - } - return []; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [refreshTrigger, fetchAssetsData], - { - watchLoading: true, - }, + // Mobile custom renderer + const mobileRenderItem = useCallback( + (asset: IEarnAvailableAsset) => ( + handleRowPress(asset)} + avatarProps={{ + src: asset.logoURI, + fallbackProps: { + borderRadius: '$full', + }, + }} + > + + {asset.symbol} + + {asset.badges?.map((badge) => ( + + {badge.tag} + + ))} + + + } + /> + + + + + + + ), + [handleRowPress], ); - if (assets.length || isLoading) { - return ( - - - {isLoading && assets.length === 0 ? ( - - - - ) : ( - + + {intl.formatMessage({ id: ETranslations.earn_available_assets })} + + ( + onPress(name)} + > + - {assets.map((asset, index) => { - const { name, logoURI, symbol, badges = [], protocols } = asset; + {name} + + + )} + /> - return ( - { - await onTokenPress?.({ - networkId: protocols[0]?.networkId || '', - accountId: account?.id ?? '', - indexedAccountId: indexedAccount?.id, - symbol, - protocols, - }); - }} - avatarProps={{ - src: logoURI, - fallbackProps: { - borderRadius: '$full', - }, - ...(media.gtLg - ? { - size: '$8', - } - : {}), - }} - {...(media.gtLg - ? { - drillIn: true, - mx: '$0', - px: '$4', - borderRadius: '$0', - } - : {})} - {...(index !== 0 && media.gtLg - ? { - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: '$borderSubdued', - } - : {})} - > - - - {symbol} - - - {badges.map((badge) => ( - - {badge.tag} - - ))} - - - } - /> - - - - - - - ); - })} - - )} - - {faqList?.length ? ( - - - - ) : null} - - ); - } - return null; + + data={assets ?? []} + columns={columns} + keyExtractor={(asset) => asset.symbol} + withHeader={media.gtSm} + defaultSortKey="yield" + defaultSortDirection="desc" + onPressRow={(asset) => void handleRowPress(asset)} + mobileRenderItem={mobileRenderItem} + enableDrillIn + isLoading={Boolean(isLoading && assets.length === 0)} + /> + + ); } diff --git a/packages/kit/src/views/Earn/components/BannerItemV2.tsx b/packages/kit/src/views/Earn/components/BannerItemV2.tsx new file mode 100644 index 000000000000..5c45c833cdae --- /dev/null +++ b/packages/kit/src/views/Earn/components/BannerItemV2.tsx @@ -0,0 +1,55 @@ +import { Image, SizableText, Stack, useMedia } from '@onekeyhq/components'; +import type { IDiscoveryBanner } from '@onekeyhq/shared/types/discovery'; + +const BANNER_TITLE_OFFSET = { + desktop: '$5', + mobile: '$10', +}; + +export function BannerItemV2({ + item, + onPress, +}: { + item: IDiscoveryBanner & { imgUrl?: string; titleTextProps?: any }; + onPress: (item: IDiscoveryBanner) => void; +}) { + const media = useMedia(); + + return ( + void onPress(item)} + > + + + {item.title?.split(/\n|\\n/).map((text, index) => ( + + {text} + + ))} + + + ); +} diff --git a/packages/kit/src/views/Earn/components/BannerV2.tsx b/packages/kit/src/views/Earn/components/BannerV2.tsx new file mode 100644 index 000000000000..78ddebd58486 --- /dev/null +++ b/packages/kit/src/views/Earn/components/BannerV2.tsx @@ -0,0 +1,61 @@ +import { useMemo } from 'react'; + +import { Carousel, Skeleton, Stack } from '@onekeyhq/components'; +import type { IDiscoveryBanner } from '@onekeyhq/shared/types/discovery'; + +import { BannerItemV2 } from './BannerItemV2'; + +interface IBannerV2Props { + data?: IDiscoveryBanner[]; + onBannerPress: (item: IDiscoveryBanner) => void; +} + +export function BannerV2({ data, onBannerPress }: IBannerV2Props) { + const content = useMemo(() => { + // Only show skeleton if data is undefined + const shouldShowSkeleton = data === undefined; + + if (shouldShowSkeleton) { + return ( + + + + ); + } + + if (data) { + return data.length ? ( + { + const noPadding = data.length > 0 && data[data.length - 1] === item; + + return ( + + + + ); + }} + autoPlayInterval={3000} + loop + showPagination + /> + ) : null; + } + + return null; + }, [data, onBannerPress]); + + return content; +} diff --git a/packages/kit/src/views/Earn/components/EarnBlockedOverview.tsx b/packages/kit/src/views/Earn/components/EarnBlockedOverview.tsx new file mode 100644 index 000000000000..f58e1b774984 --- /dev/null +++ b/packages/kit/src/views/Earn/components/EarnBlockedOverview.tsx @@ -0,0 +1,62 @@ +import { useIntl } from 'react-intl'; + +import type { IKeyOfIcons } from '@onekeyhq/components'; +import { Button, Empty, Page } from '@onekeyhq/components'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { ETabRoutes } from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; + +import { TabPageHeader } from '../../../components/TabPageHeader'; + +export function EarnBlockedOverview(props: { + showHeader?: boolean; + showContent?: boolean; + icon: IKeyOfIcons; + title: string; + description: string; + refresh: () => Promise; + refreshing: boolean; +}) { + const intl = useIntl(); + const { + title, + description, + icon, + refresh, + refreshing, + showHeader, + showContent, + } = props; + + return ( + + {showHeader ? ( + + ) : null} + + + {intl.formatMessage({ + id: ETranslations.global_refresh, + })} + + } + /> + + + ); +} diff --git a/packages/kit/src/views/Earn/components/EarnMainTabs.tsx b/packages/kit/src/views/Earn/components/EarnMainTabs.tsx new file mode 100644 index 000000000000..a45dab67222f --- /dev/null +++ b/packages/kit/src/views/Earn/components/EarnMainTabs.tsx @@ -0,0 +1,142 @@ +import { useEffect, useMemo, useRef } from 'react'; + +import { useIntl } from 'react-intl'; + +import type { ITabContainerRef } from '@onekeyhq/components'; +import { RefreshControl, Tabs, YStack } from '@onekeyhq/components'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import type { EAvailableAssetsTypeEnum } from '@onekeyhq/shared/types/earn'; + +import { FAQContent } from './FAQContent'; +import { PortfolioTabContent } from './PortfolioTabContent'; +import { ProtocolsTabContent } from './ProtocolsTabContent'; + +export function EarnMainTabs({ + isMobile, + faqList, + isFaqLoading = false, + isAccountsLoading, + refreshEarnAccounts, + containerProps, + defaultTab, + onTabChange, +}: { + isMobile: boolean; + faqList: Array<{ question: string; answer: string }>; + isFaqLoading?: boolean; + isAccountsLoading?: boolean; + refreshEarnAccounts?: () => void; + containerProps?: any; + defaultTab?: 'assets' | 'portfolio' | 'faqs'; + onTabChange?: (tab: 'assets' | 'portfolio' | 'faqs') => void; +}) { + const intl = useIntl(); + const tabsRef = useRef(null); + + const tabNames = useMemo( + () => ({ + assets: intl.formatMessage({ + id: ETranslations.earn_available_assets, + }), + portfolio: intl.formatMessage({ + id: ETranslations.earn_portfolio, + }), + faqs: intl.formatMessage({ id: ETranslations.global_faqs }), + }), + [intl], + ); + + const initialTabName = useMemo(() => { + if (defaultTab === 'portfolio') return tabNames.portfolio; + if (defaultTab === 'faqs') return tabNames.faqs; + return tabNames.assets; + }, [defaultTab, tabNames]); + + // Switch tab when defaultTab changes (from route navigation) + useEffect(() => { + if (defaultTab && tabsRef.current) { + const targetTabName = initialTabName; + const currentTabName = tabsRef.current.getFocusedTab(); + if (currentTabName !== targetTabName) { + tabsRef.current.jumpToTab(targetTabName); + } + } + }, [defaultTab, initialTabName]); + + const refreshControl = + isMobile && refreshEarnAccounts && isAccountsLoading !== undefined ? ( + + ) : undefined; + + return ( + { + const handleTabPress = (name: string) => { + tabBarProps.onTabPress?.(name); + if (onTabChange) { + if (name === tabNames.portfolio) { + onTabChange('portfolio'); + } else if (name === tabNames.faqs) { + onTabChange('faqs'); + } else { + onTabChange('assets'); + } + } + }; + return ; + }} + initialTabName={initialTabName} + {...containerProps} + > + + {isMobile ? ( + + + + + + ) : ( + + + + + + )} + + + {isMobile ? ( + + + + + + ) : ( + + + + + + )} + + + {isMobile ? ( + + + + + + ) : ( + + + + + + )} + + + ); +} diff --git a/packages/kit/src/views/Earn/components/EarnPageContainer.tsx b/packages/kit/src/views/Earn/components/EarnPageContainer.tsx new file mode 100644 index 000000000000..896e3f2cb815 --- /dev/null +++ b/packages/kit/src/views/Earn/components/EarnPageContainer.tsx @@ -0,0 +1,94 @@ +import { useCallback, useMemo } from 'react'; + +import type { IBreadcrumbProps } from '@onekeyhq/components'; +import { + Breadcrumb, + NavBackButton, + Page, + ScrollView, + XStack, + YStack, + useMedia, +} from '@onekeyhq/components'; +import { TabPageHeader } from '@onekeyhq/kit/src/components/TabPageHeader'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import type { ETabRoutes } from '@onekeyhq/shared/src/routes'; +import type { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; + +import { EARN_PAGE_MAX_WIDTH } from '../EarnConfig'; + +import type { RefreshControlProps } from 'react-native'; + +interface IEarnPageContainerProps { + pageTitle?: React.ReactNode; + header?: React.ReactNode; + children: React.ReactNode; + breadcrumbProps?: IBreadcrumbProps; + sceneName: EAccountSelectorSceneName; + tabRoute: ETabRoutes; + refreshControl?: React.ReactElement; + showBackButton?: boolean; + footer?: React.ReactNode; +} + +export function EarnPageContainer({ + pageTitle, + children, + breadcrumbProps, + sceneName, + tabRoute, + refreshControl, + showBackButton = false, + footer, + header, +}: IEarnPageContainerProps) { + const media = useMedia(); + const navigation = useAppNavigation(); + + const handleBack = useCallback(() => { + navigation.pop(); + }, [navigation]); + + const customHeaderLeft = useMemo(() => { + if (showBackButton) { + return ( + + + {pageTitle} + + ); + } + return pageTitle ? ( + + {pageTitle} + + ) : null; + }, [pageTitle, showBackButton, handleBack]); + + return ( + + + + + + + {breadcrumbProps && media.gtSm ? ( + + ) : null} + {header ? <>{header} : null} + + {children} + + + + {footer} + + ); +} diff --git a/packages/kit/src/views/Earn/components/FAQContent.tsx b/packages/kit/src/views/Earn/components/FAQContent.tsx new file mode 100644 index 000000000000..5b525f63c8b6 --- /dev/null +++ b/packages/kit/src/views/Earn/components/FAQContent.tsx @@ -0,0 +1,104 @@ +import { useIntl } from 'react-intl'; + +import { + Accordion, + Icon, + SizableText, + Skeleton, + Stack, + YStack, +} from '@onekeyhq/components'; + +function FAQPanelSkeleton() { + return ( + + + + {Array.from({ length: 4 }).map((_, index) => ( + + + + ))} + + + ); +} + +export function FAQContent({ + faqList, + isLoading = false, +}: { + faqList?: Array<{ question: string; answer: string }>; + isLoading?: boolean; +}) { + const intl = useIntl(); + + if (isLoading) { + return ; + } + + if (!faqList?.length) { + return null; + } + + return ( + + {faqList.map(({ question, answer }, index) => ( + + + {({ open }: { open: boolean }) => ( + <> + + {question} + + + + + + )} + + + + + {answer} + + + + + ))} + + ); +} diff --git a/packages/kit/src/views/Earn/components/FAQPanel.tsx b/packages/kit/src/views/Earn/components/FAQPanel.tsx deleted file mode 100644 index 85b8bb7f7485..000000000000 --- a/packages/kit/src/views/Earn/components/FAQPanel.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useIntl } from 'react-intl'; - -import { - Accordion, - Icon, - SizableText, - Skeleton, - Stack, - YStack, -} from '@onekeyhq/components'; -import { ETranslations } from '@onekeyhq/shared/src/locale'; - -function FAQPanelSkeleton() { - return ( - - - - {Array.from({ length: 4 }).map((_, index) => ( - - - - ))} - - - ); -} - -export function FAQPanel({ - faqList, - isLoading = false, -}: { - faqList?: Array<{ question: string; answer: string }>; - isLoading?: boolean; -}) { - const intl = useIntl(); - - if (isLoading) { - return ; - } - - if (!faqList?.length) { - return null; - } - - return ( - - - {intl.formatMessage({ id: ETranslations.global_faqs })} - - - - {faqList.map(({ question, answer }, index) => ( - - - {({ open }: { open: boolean }) => ( - <> - - {question} - - - - - - )} - - - - - {answer} - - - - - ))} - - - - ); -} diff --git a/packages/kit/src/views/Earn/components/Overview.tsx b/packages/kit/src/views/Earn/components/Overview.tsx new file mode 100644 index 000000000000..d89af65db533 --- /dev/null +++ b/packages/kit/src/views/Earn/components/Overview.tsx @@ -0,0 +1,431 @@ +import { useCallback, useMemo, useState } from 'react'; + +import { isEmpty } from 'lodash'; +import { useIntl } from 'react-intl'; + +import { + Divider, + Icon, + IconButton, + NumberSizeableText, + Popover, + SizableText, + Stack, + XStack, + YStack, + useMedia, +} from '@onekeyhq/components'; +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import { useSettingsPersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; +import { getNetworkIdsMap } from '@onekeyhq/shared/src/config/networkIds'; +import { + EAppEventBusNames, + appEventBus, +} from '@onekeyhq/shared/src/eventBus/appEventBus'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import { EModalRoutes, EModalStakingRoutes } from '@onekeyhq/shared/src/routes'; +import { ESpotlightTour } from '@onekeyhq/shared/src/spotlight'; +import type { + IEarnAlert, + IEarnSummaryV2, +} from '@onekeyhq/shared/types/staking'; + +import { ListItem } from '../../../components/ListItem'; +import { Token } from '../../../components/Token'; +import useAppNavigation from '../../../hooks/useAppNavigation'; +import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; +import { + useEarnActions, + useEarnAtom, +} from '../../../states/jotai/contexts/earn'; +import { EarnActionIcon } from '../../Staking/components/ProtocolDetails/EarnActionIcon'; +import { EarnText } from '../../Staking/components/ProtocolDetails/EarnText'; +import { getNumberColor } from '../utils/getNumberColor'; + +const Rebate = ({ + rebateData, + handleHistoryPress, +}: { + rebateData?: IEarnSummaryV2; + handleHistoryPress: () => void; +}) => { + const intl = useIntl(); + const [open, setOpen] = useState(false); + + const handleHistoryClick = useCallback(() => { + setOpen(false); + handleHistoryPress(); + }, [handleHistoryPress]); + + const itemRender = useCallback( + ({ + children, + key, + needDivider, + }: { + children: React.ReactNode; + key: string | number; + needDivider?: boolean; + }) => { + return ( + <> + + {children} + + {needDivider ? : null} + + ); + }, + [], + ); + + if (!rebateData) { + return null; + } + + return ( + + + + + + } + title={intl.formatMessage({ id: ETranslations.earn_referral_bonus })} + renderContent={ + + {isEmpty(rebateData?.distributed) ? null : ( + + {intl.formatMessage({ id: ETranslations.referral_distributed })} + + )} + {rebateData?.distributed.map((item, index) => { + const needDivider = + index === rebateData.distributed.length - 1 && + !isEmpty(rebateData?.undistributed); + + return itemRender({ + key: index, + needDivider, + children: ( + <> + + + + + + + ), + }); + })} + {isEmpty(rebateData?.undistributed) ? null : ( + + {intl.formatMessage({ + id: ETranslations.referral_undistributed, + })} + + )} + {rebateData?.undistributed.map((item, index) => { + return itemRender({ + key: index, + children: ( + + + + + + + + ), + }); + })} + + + + + } + /> + + ); +}; + +export const Overview = ({ + isLoading, + onRefresh, +}: { + isLoading: boolean; + onRefresh: () => void; +}) => { + const media = useMedia(); + + const { + activeAccount: { account, indexedAccount }, + } = useActiveAccount({ num: 0 }); + const actions = useEarnActions(); + const allNetworkId = getNetworkIdsMap().onekeyall; + const totalFiatMapKey = useMemo( + () => + actions.current.buildEarnAccountsKey({ + accountId: account?.id, + indexAccountId: indexedAccount?.id, + networkId: allNetworkId, + }), + [account?.id, actions, allNetworkId, indexedAccount?.id], + ); + const [{ earnAccount }] = useEarnAtom(); + const [settings] = useSettingsPersistAtom(); + const totalFiatValue = useMemo( + () => earnAccount?.[totalFiatMapKey]?.totalFiatValue || '0', + [earnAccount, totalFiatMapKey], + ); + const earnings24h = useMemo( + () => earnAccount?.[totalFiatMapKey]?.earnings24h || '0', + [earnAccount, totalFiatMapKey], + ); + const evmNetworkId = useMemo(() => getNetworkIdsMap().eth, []); + const evmAccount = useMemo(() => { + return earnAccount?.[totalFiatMapKey]?.accounts.find( + (item) => item.networkId === evmNetworkId, + ); + }, [earnAccount, totalFiatMapKey, evmNetworkId]); + + const navigation = useAppNavigation(); + const intl = useIntl(); + + // Fetch rebate data for popover + const { result: rebateData, isLoading: isRebateLoading } = + usePromiseResult(async () => { + if (!evmAccount) return null; + return backgroundApiProxy.serviceStaking.getEarnSummaryV2({ + accountAddress: evmAccount.accountAddress, + networkId: evmAccount.networkId, + }); + }, [evmAccount]); + + const shouldShowReferralBonus = useMemo( + () => !!evmAccount && !isRebateLoading && !!rebateData, + [evmAccount, isRebateLoading, rebateData], + ); + + const handleHistoryPress = useCallback(async () => { + if (!evmAccount || !account?.id) return; + const currentEarnAccount = + await backgroundApiProxy.serviceStaking.getEarnAccount({ + accountId: account.id, + indexedAccountId: indexedAccount?.id || '', + networkId: evmNetworkId, + btcOnlyTaproot: true, + }); + navigation.pushModal(EModalRoutes.StakingModal, { + screen: EModalStakingRoutes.HistoryList, + params: { + title: intl.formatMessage({ + id: ETranslations.referral_reward_history, + }), + alerts: [ + { + key: ESpotlightTour.earnRewardHistory, + badge: 'info', + alert: intl.formatMessage({ + id: ETranslations.earn_reward_distribution_schedule, + }), + } as IEarnAlert, + ], + accountId: currentEarnAccount?.account.id || '', + networkId: evmNetworkId, + filterType: 'rebate', + }, + }); + }, [ + navigation, + evmAccount, + account?.id, + indexedAccount?.id, + evmNetworkId, + intl, + ]); + + const handleRefresh = useCallback(() => { + onRefresh(); + void backgroundApiProxy.serviceStaking.clearAvailableAssetsCache(); + actions.current.triggerRefresh(); + // Trigger Portfolio refresh as well + appEventBus.emit(EAppEventBusNames.RefreshEarnPortfolio, undefined); + }, [onRefresh, actions]); + + return ( + + {/* total value */} + + + {intl.formatMessage({ id: ETranslations.earn_total_staked_value })} + + + + {totalFiatValue} + + {platformEnv.isNative ? null : ( + + )} + + + {/* 24h earnings */} + + + {earnings24h} + + + + {intl.formatMessage({ id: ETranslations.earn_24h_earnings })} + + + } + title={intl.formatMessage({ + id: ETranslations.earn_24h_earnings, + })} + renderContent={ + + {intl.formatMessage({ + id: ETranslations.earn_24h_earnings_tooltip, + })} + + } + /> + + + + {shouldShowReferralBonus ? ( + + ) : null} + + ); +}; diff --git a/packages/kit/src/views/Earn/components/PortfolioTabContent.tsx b/packages/kit/src/views/Earn/components/PortfolioTabContent.tsx new file mode 100644 index 000000000000..b14281752cd2 --- /dev/null +++ b/packages/kit/src/views/Earn/components/PortfolioTabContent.tsx @@ -0,0 +1,726 @@ +import { memo, useCallback, useEffect, useMemo } from 'react'; + +import { isEmpty } from 'lodash'; +import { useIntl } from 'react-intl'; + +import { + Button, + Divider, + Empty, + SizableText, + Skeleton, + Stack, + XStack, + YStack, + useMedia, +} from '@onekeyhq/components'; +import { NumberSizeableText } from '@onekeyhq/components/src/content/NumberSizeableText'; +import type { ITableColumn } from '@onekeyhq/kit/src/components/ListView/TableList'; +import { TableList } from '@onekeyhq/kit/src/components/ListView/TableList'; +import { Token } from '@onekeyhq/kit/src/components/Token'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { + EAppEventBusNames, + appEventBus, +} from '@onekeyhq/shared/src/eventBus/appEventBus'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { + EModalRoutes, + EModalStakingRoutes, + ERootRoutes, + ETabEarnRoutes, + ETabRoutes, +} from '@onekeyhq/shared/src/routes'; +import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; +import type { IEarnPortfolioInvestment } from '@onekeyhq/shared/types/staking'; + +import { useCurrency } from '../../../components/Currency'; +import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; +import { EarnText } from '../../Staking/components/ProtocolDetails/EarnText'; +import { EarnTooltip } from '../../Staking/components/ProtocolDetails/EarnTooltip'; +import { buildLocalTxStatusSyncId } from '../../Staking/utils/utils'; +import { useEarnPortfolio } from '../hooks/useEarnPortfolio'; +import { usePortfolioAction } from '../hooks/usePortfolioAction'; + +const WrappedActionButton = ({ + asset, + reward, + stakedSymbol, + rewardSymbol, +}: { + asset: + | IEarnPortfolioInvestment['assets'][number] + | IEarnPortfolioInvestment['airdropAssets'][number]; + reward: + | IEarnPortfolioInvestment['assets'][number]['rewardAssets'][number] + | IEarnPortfolioInvestment['airdropAssets'][number]['airdropAssets'][number]; + stakedSymbol?: string; + rewardSymbol?: string; +}) => { + const { activeAccount } = useActiveAccount({ num: 0 }); + const { account, indexedAccount } = activeAccount; + + // For staking config lookup, use: + // - stakedSymbol for airdrops (the token that was staked to earn rewards) + // - asset.token.info.symbol for normal claims (the staked token itself) + const symbolForConfig = stakedSymbol || asset.token.info.symbol; + + const { loading, handleAction } = usePortfolioAction({ + accountId: account?.id || '', + networkId: asset.metadata.network.networkId, + indexedAccountId: indexedAccount?.id, + symbol: symbolForConfig, + provider: asset.metadata.protocol.providerDetail.code, + vault: asset.metadata.protocol.vault, + providerLogoURI: asset.metadata.protocol.providerDetail.logoURI, + stakeTag: buildLocalTxStatusSyncId({ + providerName: asset.metadata.protocol.providerDetail.code, + tokenSymbol: symbolForConfig, + }), + }); + + return ( + + ); +}; + +const DepositField = ({ + asset, +}: { + asset: IEarnPortfolioInvestment['assets'][number]; +}) => { + return ( + + + + + + + + {asset.metadata.protocol.vaultName ? ( + + {asset.metadata.protocol.vaultName} + + ) : null} + + + ); +}; + +const AssetStatusField = ({ + asset, +}: { + asset: IEarnPortfolioInvestment['assets'][number]; +}) => { + return ( + + {asset.assetsStatus?.map((status, index) => ( + + + + + + ))} + + ); +}; + +const ActionField = ({ + asset, +}: { + asset: IEarnPortfolioInvestment['assets'][number]; +}) => { + return ( + + {asset.rewardAssets?.map((reward, index) => ( + + + + + + ))} + + ); +}; + +const ProtocolHeader = ({ + portfolioItem, +}: { + portfolioItem: IEarnPortfolioInvestment; +}) => { + const intl = useIntl(); + const currencyInfo = useCurrency(); + const media = useMedia(); + + return ( + + + + + {portfolioItem.protocol.providerDetail.name} + + + + + {intl.formatMessage({ id: ETranslations.earn_total_staked_value })} + + + {portfolioItem.totalFiatValue} + + + + {isEmpty(portfolioItem.airdropAssets) || + portfolioItem.airdropAssets?.every((airdrop) => + isEmpty(airdrop.airdropAssets), + ) ? null : ( + + {portfolioItem.airdropAssets?.map((airdrop, index) => { + // Skip if this airdrop has no airdropAssets + if (isEmpty(airdrop.airdropAssets)) { + return null; + } + + // For airdrops, we need the staked token symbol to look up staking config + // Use the first staked asset's symbol from the same protocol + const stakedSymbol = portfolioItem.assets[0]?.token.info.symbol; + const Wrapper = media.gtSm ? XStack : YStack; + + return ( + + {media.gtMd ? ( + + ) : null} + {airdrop.airdropAssets.map((reward, rewardIndex) => { + const needDivider = + rewardIndex < airdrop.airdropAssets.length - 1 && + media.gtMd; + + return ( + + + + + {reward.button ? ( + + ) : null} + {needDivider ? ( + + ) : null} + + ); + })} + + ); + })} + + )} + + ); +}; + +const PortfolioItemComponent = ({ + portfolioItem, +}: { + portfolioItem: IEarnPortfolioInvestment; +}) => { + const intl = useIntl(); + const media = useMedia(); + + const columns: ITableColumn[] = + useMemo(() => { + return [ + { + key: 'deposits', + label: intl.formatMessage({ id: ETranslations.earn_deposited }), + flex: 1.5, + priority: 5, + render: (asset) => , + }, + { + key: 'Est. 24h earnings', + label: intl.formatMessage({ id: ETranslations.earn_24h_earnings }), + flex: 1, + priority: 1, + render: (asset) => ( + + ), + }, + { + key: 'Asset status', + label: intl.formatMessage({ id: ETranslations.global_status }), + flex: 1.5, + priority: 3, + render: (asset) => , + }, + { + key: 'Claimable', + label: intl.formatMessage({ id: ETranslations.earn_claimable }), + flex: 1.5, + priority: 3, + render: (asset) => { + if (isEmpty(asset.rewardAssets)) { + return ( + + ); + } + return ; + }, + }, + ]; + }, [intl]); + + const appNavigation = useAppNavigation(); + const { activeAccount } = useActiveAccount({ num: 0 }); + const { account, indexedAccount } = activeAccount; + + const handleRowPress = useCallback( + async (asset: IEarnPortfolioInvestment['assets'][number]) => { + const symbol = asset.token.info.symbol; + appNavigation.navigate(ERootRoutes.Main, { + screen: ETabRoutes.Earn, + params: { + screen: ETabEarnRoutes.EarnProtocolDetails, + params: { + indexedAccountId: indexedAccount?.id, + accountId: account?.id, + networkId: asset.metadata.network.networkId, + symbol, + provider: asset.metadata.protocol.providerDetail.code, + vault: asset.metadata.protocol.vault, + }, + }, + }); + }, + [appNavigation, account?.id, indexedAccount?.id], + ); + + const handleManagePress = useCallback( + async (asset: IEarnPortfolioInvestment['assets'][number]) => { + const symbol = asset.token.info.symbol; + // if (symbol === 'USDe') { + // appNavigation.navigate(ERootRoutes.Main, { + // screen: ETabRoutes.Earn, + // params: { + // screen: ETabEarnRoutes.EarnProtocolDetails, + // params: { + // indexedAccountId: indexedAccount?.id, + // accountId: account?.id, + // networkId: asset.metadata.network.networkId, + // symbol, + // provider: asset.metadata.protocol.providerDetail.code, + // vault: asset.metadata.protocol.vault, + // }, + // }, + // }); + + // return; + // } + appNavigation.pushModal(EModalRoutes.StakingModal, { + screen: EModalStakingRoutes.ManagePosition, + params: { + networkId: asset.metadata.network.networkId, + symbol, + provider: asset.metadata.protocol.providerDetail.code, + vault: asset.metadata.protocol.vault, + }, + }); + }, + [appNavigation], + ); + + const showTable = useMemo( + () => !isEmpty(portfolioItem.assets), + [portfolioItem.assets], + ); + + return ( + + + {showTable ? ( + + data={portfolioItem.assets} + columns={columns} + withHeader={media.gtSm} + tableLayout + defaultSortKey="deposits" + defaultSortDirection="desc" + listItemProps={{ + ai: 'flex-start', + }} + onPressRow={handleRowPress} + expandable={ + !media.gtSm + ? { + renderExpandedContent: (asset) => ( + + {/* Est. 24h earnings */} + + + + {intl.formatMessage({ + id: ETranslations.earn_24h_earnings, + })} + + + + {/* Asset status list */} + {asset.assetsStatus?.map((status, index) => ( + + + + + + ))} + + {/* Reward assets (claimable rewards) */} + {asset.rewardAssets?.map((reward, index) => ( + + + + + + + + + ))} + + {/* Buttons */} + + + + + + ), + } + : undefined + } + actions={{ + render: (asset) => { + return ( + + {asset.buttons?.map( + ( + button: { + type: string; + text: { text: string }; + disabled: boolean; + }, + index: number, + ) => { + return ( + + ); + }, + )} + + ); + }, + width: 100, + align: 'flex-end', + }} + /> + ) : null} + + ); +}; + +const PortfolioItem = memo(PortfolioItemComponent); + +// Skeleton component for loading state +const PortfolioSkeletonItem = () => { + const media = useMedia(); + + return ( + + + + + + + + {Array.from({ length: 2 }).map((_, index) => ( + + + + + + + {media.gtSm ? ( + <> + + + + + + + + + ) : null} + + ))} + + + ); +}; + +const PortfolioSkeleton = () => ( + + + + + +); + +export const PortfolioTabContent = () => { + const intl = useIntl(); + const { investments, isLoading, refresh } = useEarnPortfolio(); + + const refreshPortfolioRow = useCallback< + (payload: { + provider: string; + symbol: string; + networkId: string; + rewardSymbol?: string; + }) => void + >( + (payload) => { + if (!payload?.provider || !payload?.symbol || !payload?.networkId) { + return; + } + // Add delay to allow backend data to update after order success + void timerUtils.wait(350).then(() => { + void refresh({ + provider: payload.provider, + symbol: payload.symbol, + networkId: payload.networkId, + rewardSymbol: payload.rewardSymbol, + }); + }); + }, + [refresh], + ); + + useEffect(() => { + const handler = (payload: { + provider: string; + symbol: string; + networkId: string; + rewardSymbol?: string; + }) => { + refreshPortfolioRow(payload); + }; + const fullRefreshHandler = () => { + void refresh(); + }; + appEventBus.on(EAppEventBusNames.RefreshEarnPortfolioItem, handler); + appEventBus.on(EAppEventBusNames.RefreshEarnPortfolio, fullRefreshHandler); + return () => { + appEventBus.off(EAppEventBusNames.RefreshEarnPortfolioItem, handler); + appEventBus.off( + EAppEventBusNames.RefreshEarnPortfolio, + fullRefreshHandler, + ); + }; + }, [refreshPortfolioRow, refresh]); + + const showSkeleton = isLoading && investments.length === 0; + const showEmpty = !isLoading && investments.length === 0; + + // Show skeleton while loading initial data + if (showSkeleton) { + return ; + } + + // Show empty state when no investments + if (showEmpty) { + return ( + + ); + } + + return ( + + {investments.map((item, index) => { + const showDivider = index < investments.length - 1; + const key = `${item.protocol.providerDetail.code}_${ + item.protocol.vaultName || '' + }_${item.network.networkId}`; + + return ( + <> + + {showDivider ? : null} + + ); + })} + + ); +}; diff --git a/packages/kit/src/views/Earn/components/ProtocolsTabContent.tsx b/packages/kit/src/views/Earn/components/ProtocolsTabContent.tsx new file mode 100644 index 000000000000..8208a4c4d584 --- /dev/null +++ b/packages/kit/src/views/Earn/components/ProtocolsTabContent.tsx @@ -0,0 +1,13 @@ +import { YStack } from '@onekeyhq/components'; + +import { AvailableAssetsTabViewList } from './AvailableAssetsTabViewList'; +import { Recommended } from './Recommended'; + +export function ProtocolsTabContent() { + return ( + + + + + ); +} diff --git a/packages/kit/src/views/Earn/components/Recommended.tsx b/packages/kit/src/views/Earn/components/Recommended.tsx new file mode 100644 index 000000000000..195003c31285 --- /dev/null +++ b/packages/kit/src/views/Earn/components/Recommended.tsx @@ -0,0 +1,351 @@ +import type { PropsWithChildren } from 'react'; +import { memo, useCallback, useMemo } from 'react'; + +import { useIntl } from 'react-intl'; +import { StyleSheet } from 'react-native'; + +import type { IYStackProps } from '@onekeyhq/components'; +import { + Icon, + Image, + ScrollView, + SizableText, + Skeleton, + XStack, + YStack, + useMedia, +} from '@onekeyhq/components'; +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { getNetworkIdsMap } from '@onekeyhq/shared/src/config/networkIds'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import type { IRecommendAsset } from '@onekeyhq/shared/types/staking'; + +import { usePromiseResult } from '../../../hooks/usePromiseResult'; +import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; +import { + useEarnActions, + useEarnAtom, +} from '../../../states/jotai/contexts/earn'; +import { useToTokenProviderListPage } from '../hooks/useToTokenProviderListPage'; + +import { AprText } from './AprText'; + +function RecommendedSkeletonItem({ ...rest }: IYStackProps) { + return ( + + + + + + + + + + + + ); +} + +const RecommendedItem = memo( + ({ + token, + accountId, + indexedAccountId, + noWalletConnected, + ...rest + }: { + token?: IRecommendAsset; + accountId?: string; + indexedAccountId?: string; + noWalletConnected: boolean; + } & IYStackProps) => { + const toTokenProviderListPage = useToTokenProviderListPage(); + + const onPress = useCallback(async () => { + if (token) { + const earnAccount = + await backgroundApiProxy.serviceStaking.getEarnAccount({ + indexedAccountId, + accountId: accountId ?? '', + networkId: token.protocols[0]?.networkId, + }); + await toTokenProviderListPage({ + indexedAccountId: + earnAccount?.account.indexedAccountId || indexedAccountId, + accountId: earnAccount?.accountId || accountId || '', + networkId: token.protocols[0]?.networkId, + symbol: token.symbol, + protocols: token.protocols, + logoURI: token.logoURI, + }); + } + }, [accountId, indexedAccountId, toTokenProviderListPage, token]); + + if (!token) { + return ; + } + + return ( + + + + + + + + } + /> + + {token.symbol} + + + + + + {!noWalletConnected ? ( + + {token?.available?.text} + + ) : null} + + + + ); + }, +); + +RecommendedItem.displayName = 'RecommendedItem'; + +function RecommendedContainer({ children }: PropsWithChildren) { + const intl = useIntl(); + return ( + + {/* since the children have been used negative margin, so we should use zIndex to make sure the trigger of popover is on top of the children */} + + + {intl.formatMessage({ id: ETranslations.market_trending })} + + + {children} + + ); +} + +export function Recommended() { + const { md } = useMedia(); + const allNetworkId = getNetworkIdsMap().onekeyall; + const { + activeAccount: { account, indexedAccount }, + } = useActiveAccount({ num: 0 }); + const [{ refreshTrigger = 0, recommendedTokens = [] }] = useEarnAtom(); + const actions = useEarnActions(); + + const noWalletConnected = useMemo( + () => !account && !indexedAccount, + [account, indexedAccount], + ); + + // Fetch new tokens in background and update cache + usePromiseResult( + async () => { + const recommendedAssets = + await backgroundApiProxy.serviceStaking.fetchAllNetworkAssetsV2({ + accountId: account?.id ?? '', + networkId: allNetworkId, + indexedAccountId: account?.indexedAccountId || indexedAccount?.id, + }); + + const newTokens = recommendedAssets?.tokens || []; + + // Update cache with new tokens + actions.current.updateRecommendedTokens(newTokens); + + return newTokens; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + account?.id, + allNetworkId, + account?.indexedAccountId, + indexedAccount?.id, + refreshTrigger, + ], + { + watchLoading: true, + }, + ); + + // Render skeleton when loading and no data + const shouldShowSkeleton = recommendedTokens.length === 0; + if (shouldShowSkeleton) { + return ( + + {/* Desktop/Extension with larger screen: 4 items per row */} + {platformEnv.isNative ? ( + // Mobile: horizontal scrolling skeleton + + + {Array.from({ length: 4 }).map((_, index) => ( + + + + ))} + + + ) : ( + // Desktop/Extension: grid layout + + {Array.from({ length: 4 }).map((_, index) => ( + + + + ))} + + )} + + ); + } + + // Render actual tokens + if (recommendedTokens.length) { + return ( + + {platformEnv.isNative ? ( + // Mobile: horizontal scrolling + + + {recommendedTokens.map((token) => ( + + + + ))} + + + ) : ( + // Desktop/Extension: grid layout + + {recommendedTokens.map((token) => ( + + + + ))} + + )} + + ); + } + return null; +} diff --git a/packages/kit/src/views/Earn/components/showProtocolListDialog.tsx b/packages/kit/src/views/Earn/components/showProtocolListDialog.tsx index ce93b6ed080d..75da8c46151c 100644 --- a/packages/kit/src/views/Earn/components/showProtocolListDialog.tsx +++ b/packages/kit/src/views/Earn/components/showProtocolListDialog.tsx @@ -149,7 +149,6 @@ function ProtocolListDialogContent({ const groupedData = groupProtocolsByGroup(data); setProtocolData(groupedData); } catch (error) { - console.error('Failed to fetch protocol data:', error); setProtocolData([]); } finally { setIsLoading(false); @@ -313,8 +312,6 @@ export function showProtocolListDialog({ vault?: string; }) => Promise; }) { - console.log('showProtocolListDialog called with:', { symbol }); - const dialog = Dialog.show({ title: appLocale.intl.formatMessage( { @@ -361,7 +358,7 @@ export function showProtocolListDialog({ : undefined, }); } catch (error) { - console.error('Failed to select protocol:', error); + // Handle error silently } finally { void dialog.close(); } diff --git a/packages/kit/src/views/Earn/earnUtils.ts b/packages/kit/src/views/Earn/earnUtils.ts index 185f61c1b225..8dd2e38554dc 100644 --- a/packages/kit/src/views/Earn/earnUtils.ts +++ b/packages/kit/src/views/Earn/earnUtils.ts @@ -3,11 +3,20 @@ import { WEB_APP_URL_DEV, } from '@onekeyhq/shared/src/config/appConfig'; import { getNetworkIdsMap } from '@onekeyhq/shared/src/config/networkIds'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; -import { EModalRoutes, EModalStakingRoutes } from '@onekeyhq/shared/src/routes'; +import { + EModalRoutes, + EModalStakingRoutes, + ERootRoutes, + ETabEarnRoutes, + ETabRoutes, +} from '@onekeyhq/shared/src/routes'; +import type { IEarnAvailableAssetProtocol } from '@onekeyhq/shared/types/earn'; import backgroundApiProxy from '../../background/instance/backgroundApiProxy'; +import type useAppNavigation from '../../hooks/useAppNavigation'; import type { IAppNavigation } from '../../hooks/useAppNavigation'; const NetworkNameToIdMap: Record = { @@ -67,16 +76,19 @@ export const EarnNavigation = { indexedAccountId, networkId, }); - navigation.pushModal(EModalRoutes.StakingModal, { - screen: EModalStakingRoutes.ProtocolDetailsV2, + navigation.navigate(ERootRoutes.Main, { + screen: ETabRoutes.Earn, params: { - accountId: earnAccount?.accountId || accountId || '', - networkId, - indexedAccountId: - earnAccount?.account.indexedAccountId || indexedAccountId, - symbol, - provider, - vault, + screen: ETabEarnRoutes.EarnProtocolDetails, + params: { + accountId: earnAccount?.accountId || accountId || '', + networkId, + indexedAccountId: + earnAccount?.account.indexedAccountId || indexedAccountId, + symbol, + provider, + vault, + }, }, }); }, @@ -107,7 +119,7 @@ export const EarnNavigation = { }); }, - // generate share link + // generate share link (for modal) generateShareLink({ networkId, symbol, @@ -142,4 +154,99 @@ export const EarnNavigation = { ? `${origin}${baseUrl}?${queryString}` : `${origin}${baseUrl}`; }, + + // generate earn share link (for EarnProtocolDetails page) + generateEarnShareLink({ + networkId, + symbol, + provider, + vault, + isDevMode = false, + }: { + networkId: string; + symbol: string; + provider: string; + vault?: string; + isDevMode?: boolean; + }): string { + let origin = WEB_APP_URL; + if (platformEnv.isWeb) { + origin = globalThis.location.origin; + } + if (!platformEnv.isWeb && isDevMode) { + origin = WEB_APP_URL_DEV; + } + + const networkName = EarnNetworkUtils.getShareNetworkParam(networkId); + const baseUrl = `/earn/${networkName}/${symbol.toLowerCase()}/${provider.toLowerCase()}`; + const queryParams = new URLSearchParams(); + + if (vault) { + queryParams.append('vault', vault); + } + + const queryString = queryParams.toString(); + return queryString + ? `${origin}${baseUrl}?${queryString}` + : `${origin}${baseUrl}`; + }, + + toTokenProviderListPage: async ( + navigation: ReturnType, + { + networkId, + accountId, + indexedAccountId, + symbol, + protocols, + logoURI, + }: { + networkId: string; + accountId: string; + indexedAccountId?: string; + symbol: string; + protocols: IEarnAvailableAssetProtocol[]; + logoURI?: string; + }, + ) => { + defaultLogger.staking.page.selectAsset({ tokenSymbol: symbol }); + + if (protocols.length === 1) { + const earnAccount = + await backgroundApiProxy.serviceStaking.getEarnAccount({ + accountId, + indexedAccountId, + networkId, + }); + const protocol = protocols[0]; + navigation.navigate(ERootRoutes.Main, { + screen: ETabRoutes.Earn, + params: { + screen: ETabEarnRoutes.EarnProtocolDetails, + params: { + networkId: protocol.networkId, + accountId: earnAccount?.accountId || accountId, + indexedAccountId: + earnAccount?.account.indexedAccountId || indexedAccountId, + symbol, + provider: protocol.provider, + vault: protocol.vault, + }, + }, + }); + return; + } + + navigation.navigate(ERootRoutes.Main, { + screen: ETabRoutes.Earn, + params: { + screen: ETabEarnRoutes.EarnProtocols, + params: { + symbol, + filterNetworkId: undefined, + logoURI: encodeURIComponent(logoURI ?? ''), + }, + }, + }); + }, }; diff --git a/packages/kit/src/views/Earn/hooks/useBannerInfo.ts b/packages/kit/src/views/Earn/hooks/useBannerInfo.ts new file mode 100644 index 000000000000..87b0cb32971c --- /dev/null +++ b/packages/kit/src/views/Earn/hooks/useBannerInfo.ts @@ -0,0 +1,29 @@ +import { useCallback } from 'react'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; + +import { useEarnActions } from '../../../states/jotai/contexts/earn'; +import { useEarnAtom } from '../../../states/jotai/contexts/earn/atoms'; + +export const useBannerInfo = () => { + const actions = useEarnActions(); + const [earnData] = useEarnAtom(); + + const refetchBanners = useCallback(async () => { + const bannerResult = + await backgroundApiProxy.serviceStaking.fetchEarnHomePageBannerList(); + const transformedBanners = + bannerResult?.map((i) => ({ + ...i, + imgUrl: i.src, + title: i.title || '', + titleTextProps: { + size: '$headingMd', + }, + })) || []; + + actions.current.updateBanners(transformedBanners); + }, [actions]); + + return { earnBanners: earnData.banners, refetchBanners }; +}; diff --git a/packages/kit/src/views/Earn/hooks/useBlockRegion.ts b/packages/kit/src/views/Earn/hooks/useBlockRegion.ts new file mode 100644 index 000000000000..6bebdf7f3eb2 --- /dev/null +++ b/packages/kit/src/views/Earn/hooks/useBlockRegion.ts @@ -0,0 +1,23 @@ +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; + +import { usePromiseResult } from '../../../hooks/usePromiseResult'; + +export const useBlockRegion = () => { + const { + isLoading: isFetchingBlockResult, + run: refreshBlockResult, + result: blockResult, + } = usePromiseResult( + async () => { + const blockData = + await backgroundApiProxy.serviceStaking.getBlockRegion(); + return { blockData }; + }, + [], + { + revalidateOnFocus: true, + }, + ); + + return { isFetchingBlockResult, refreshBlockResult, blockResult }; +}; diff --git a/packages/kit/src/views/Earn/hooks/useEarnAccounts.ts b/packages/kit/src/views/Earn/hooks/useEarnAccounts.ts new file mode 100644 index 000000000000..1ef91d11782c --- /dev/null +++ b/packages/kit/src/views/Earn/hooks/useEarnAccounts.ts @@ -0,0 +1,91 @@ +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { getNetworkIdsMap } from '@onekeyhq/shared/src/config/networkIds'; +import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; + +import { usePromiseResult } from '../../../hooks/usePromiseResult'; +import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; +import { useEarnActions } from '../../../states/jotai/contexts/earn'; + +export const useEarnAccounts = () => { + const actions = useEarnActions(); + const { activeAccount } = useActiveAccount({ num: 0 }); + const { account, indexedAccount } = activeAccount; + const allNetworkId = getNetworkIdsMap().onekeyall; + const isAccountExists = !!account; + + const { + result, + isLoading: isFetchingAccounts, + run: refreshEarnAccounts, + } = usePromiseResult( + async () => { + if (!isAccountExists && !indexedAccount?.id) { + return; + } + const totalFiatMapKey = actions.current.buildEarnAccountsKey({ + accountId: account?.id, + indexAccountId: indexedAccount?.id, + networkId: allNetworkId, + }); + + const fetchAndUpdateOverview = async () => { + if (!isAccountExists && !indexedAccount?.id) { + return; + } + + // Fetch account assets (contains accounts array) + const assetsData = + await backgroundApiProxy.serviceStaking.fetchAllNetworkAssets({ + accountId: account?.id ?? '', + networkId: allNetworkId, + indexedAccountId: account?.indexedAccountId || indexedAccount?.id, + }); + + const overviewData = + await backgroundApiProxy.serviceStaking.fetchAccountOverview({ + accountId: account?.id ?? '', + networkId: allNetworkId, + indexedAccountId: account?.indexedAccountId || indexedAccount?.id, + }); + + actions.current.updateEarnAccounts({ + key: totalFiatMapKey, + earnAccount: { + accounts: assetsData?.accounts || [], + ...overviewData, + isOverviewLoaded: true, + }, + }); + }; + + const earnAccountData = actions.current.getEarnAccount(totalFiatMapKey); + if (earnAccountData) { + await timerUtils.wait(350); + await fetchAndUpdateOverview(); + } else { + await fetchAndUpdateOverview(); + } + return { loaded: true }; + }, + [ + account?.id, + account?.indexedAccountId, + actions, + allNetworkId, + indexedAccount?.id, + isAccountExists, + ], + { + watchLoading: true, + pollingInterval: timerUtils.getTimeDurationMs({ minute: 3 }), + revalidateOnReconnect: true, + alwaysSetState: true, + }, + ); + + return { + loaded: result?.loaded, + isFetchingAccounts, + refreshEarnAccounts, + }; +}; diff --git a/packages/kit/src/views/Earn/hooks/useEarnPortfolio.ts b/packages/kit/src/views/Earn/hooks/useEarnPortfolio.ts new file mode 100644 index 000000000000..3de66207330a --- /dev/null +++ b/packages/kit/src/views/Earn/hooks/useEarnPortfolio.ts @@ -0,0 +1,535 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import BigNumber from 'bignumber.js'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { getNetworkIdsMap } from '@onekeyhq/shared/src/config/networkIds'; +import type { + IEarnInvestmentItemV2, + IEarnPortfolioInvestment, +} from '@onekeyhq/shared/types/staking'; + +import { usePromiseResult } from '../../../hooks/usePromiseResult'; +import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; + +interface IRefreshOptions { + provider?: string; + networkId?: string; + symbol?: string; + rewardSymbol?: string; +} + +type IInvestmentKey = string; +type IInvestmentMap = Map; + +// Pure utility functions +const createInvestmentKey = (item: { + provider: string; + symbol: string; + vault?: string; + networkId: string; +}): IInvestmentKey => + `${item.provider}_${item.symbol}_${item.vault || ''}_${item.networkId}`; + +const hasPositiveFiatValue = (value: string | undefined): boolean => + new BigNumber(value || '0').gt(0); + +const sortByFiatValueDesc = ( + investments: IEarnPortfolioInvestment[], +): IEarnPortfolioInvestment[] => + [...investments].sort((a, b) => { + const valueA = new BigNumber(a.totalFiatValue || '0'); + const valueB = new BigNumber(b.totalFiatValue || '0'); + return valueB.comparedTo(valueA); + }); + +const calculateTotalFiatValue = ( + investments: IEarnPortfolioInvestment[], +): BigNumber => + investments.reduce( + (sum, inv) => sum.plus(new BigNumber(inv.totalFiatValue || '0')), + new BigNumber(0), + ); + +const enrichAssetWithMetadata = ( + asset: IEarnInvestmentItemV2['assets'][number], + investment: IEarnInvestmentItemV2, +): IEarnPortfolioInvestment['assets'][number] => ({ + ...asset, + metadata: { + protocol: investment.protocol, + network: investment.network, + }, +}); + +const mergeInvestments = ( + existing: IEarnPortfolioInvestment, + incoming: IEarnPortfolioInvestment, +): IEarnPortfolioInvestment => { + const existingTotal = new BigNumber(existing.totalFiatValue || '0'); + const incomingTotal = new BigNumber(incoming.totalFiatValue || '0'); + + return { + ...existing, + assets: [...existing.assets, ...incoming.assets], + airdropAssets: [...existing.airdropAssets, ...incoming.airdropAssets], + totalFiatValue: existingTotal.plus(incomingTotal).toFixed(), + }; +}; + +const aggregateByProtocol = ( + investments: IEarnPortfolioInvestment[], +): IEarnPortfolioInvestment[] => { + const protocolMap = investments.reduce((map, investment) => { + const protocolKey = investment.protocol.providerDetail.code; + const existing = map.get(protocolKey); + + if (existing) { + map.set(protocolKey, mergeInvestments(existing, investment)); + } else { + map.set(protocolKey, { ...investment }); + } + + return map; + }, new Map()); + + return sortByFiatValueDesc(Array.from(protocolMap.values())); +}; + +// Custom hook for managing investment state +const useInvestmentState = () => { + const [investments, setInvestments] = useState( + [], + ); + const [earnTotalFiatValue, setEarnTotalFiatValue] = useState( + new BigNumber(0), + ); + const investmentMapRef = useRef(new Map()); + + const updateInvestments = useCallback((newMap: IInvestmentMap) => { + // Filter out zero-value investments (including airdrops) + const validInvestments = Array.from(newMap.values()).filter((inv) => { + // Only keep investments with positive fiat value + return hasPositiveFiatValue(inv.totalFiatValue); + }); + + const sorted = sortByFiatValueDesc(validInvestments); + setInvestments(sorted); + setEarnTotalFiatValue(calculateTotalFiatValue(sorted)); + investmentMapRef.current = new Map( + validInvestments.map((inv) => { + const firstAsset = inv.assets[0] || inv.airdropAssets[0]; + return [ + createInvestmentKey({ + provider: inv.protocol.providerDetail.code, + symbol: firstAsset?.token.info.symbol || '', + vault: inv.protocol.vault, + networkId: inv.network.networkId, + }), + inv, + ]; + }), + ); + }, []); + + const clearInvestments = useCallback(() => { + investmentMapRef.current.clear(); + setInvestments([]); + setEarnTotalFiatValue(new BigNumber(0)); + }, []); + + return { + investments, + earnTotalFiatValue, + investmentMapRef, + updateInvestments, + clearInvestments, + }; +}; + +// Custom hook for managing account state +const useAccountState = ( + account?: { id: string } | null, + indexedAccount?: { id: string } | null, +) => { + const prevAccountRef = useRef({ + accountId: account?.id, + indexedAccountId: indexedAccount?.id, + }); + const currentRequestIdRef = useRef(0); + + const accountId = account?.id; + const indexedAccountId = indexedAccount?.id; + + const hasAccountChanged = useCallback(() => { + return ( + prevAccountRef.current.accountId !== accountId || + prevAccountRef.current.indexedAccountId !== indexedAccountId + ); + }, [accountId, indexedAccountId]); + + const markAccountChange = useCallback(() => { + prevAccountRef.current = { accountId, indexedAccountId }; + currentRequestIdRef.current += 1; + }, [accountId, indexedAccountId]); + + const isRequestStale = useCallback((requestId: number) => { + return requestId !== currentRequestIdRef.current; + }, []); + + const getCurrentRequestId = useCallback( + () => currentRequestIdRef.current, + [], + ); + + return { + hasAccountChanged, + markAccountChange, + isRequestStale, + getCurrentRequestId, + }; +}; + +export const useEarnPortfolio = () => { + const { activeAccount } = useActiveAccount({ num: 0 }); + const { account, indexedAccount } = activeAccount; + const allNetworkId = getNetworkIdsMap().onekeyall; + const [isLoading, setIsLoading] = useState(true); + + const { + investments, + earnTotalFiatValue, + investmentMapRef, + updateInvestments, + clearInvestments, + } = useInvestmentState(); + + const { + hasAccountChanged, + markAccountChange, + isRequestStale, + getCurrentRequestId, + } = useAccountState(account, indexedAccount); + + // Handle account changes + useEffect(() => { + if (hasAccountChanged()) { + clearInvestments(); + } + }, [hasAccountChanged, clearInvestments]); + + const fetchInvestmentDetail = useCallback( + async ( + item: { + accountAddress: string; + networkId: string; + provider: string; + symbol: string; + vault?: string; + publicKey?: string; + }, + isAirdrop: boolean, + requestId: number, + ) => { + try { + if (isAirdrop) { + const result = + await backgroundApiProxy.serviceStaking.fetchAirdropInvestmentDetail( + item, + ); + + if (isRequestStale(requestId)) { + return null; + } + + const key = createInvestmentKey({ + provider: result.protocol.providerDetail.code, + symbol: result.assets?.[0]?.token.info.symbol || '', + vault: result.protocol.vault, + networkId: result.network.networkId, + }); + + const enrichedAirdropAssets = result.assets.map((asset) => ({ + ...asset, + metadata: { + protocol: result.protocol, + network: result.network, + }, + })); + + const investment: IEarnPortfolioInvestment = { + totalFiatValue: result.totalFiatValue, + protocol: result.protocol, + network: result.network, + assets: [], + airdropAssets: enrichedAirdropAssets, + }; + + return { key, investment }; + } + + const result = + await backgroundApiProxy.serviceStaking.fetchInvestmentDetailV2(item); + + if ( + isRequestStale(requestId) || + !hasPositiveFiatValue(result.totalFiatValue) + ) { + return null; + } + + const key = createInvestmentKey({ + provider: result.protocol.providerDetail.code, + symbol: result.assets?.[0]?.token.info.symbol || '', + vault: result.protocol.vault, + networkId: result.network.networkId, + }); + + const enrichedAssets = result.assets.map((asset) => + enrichAssetWithMetadata(asset, result), + ); + + const investment: IEarnPortfolioInvestment = { + totalFiatValue: result.totalFiatValue, + protocol: result.protocol, + network: result.network, + assets: enrichedAssets, + airdropAssets: [], + }; + + return { key, investment }; + } catch (error) { + return null; + } + }, + [isRequestStale], + ); + + const fetchAndUpdateInvestments = useCallback( + async (options?: IRefreshOptions) => { + if (!account && !indexedAccount) { + setIsLoading(false); + return; + } + + // Check if account changed and update requestId BEFORE getting current requestId + // This ensures the new fetch uses the updated requestId + if (hasAccountChanged()) { + markAccountChange(); + } + + const requestId = getCurrentRequestId(); + // Only set loading state for full refresh, not for partial refresh + const isPartialRefresh = Boolean(options); + if (!isPartialRefresh) { + setIsLoading(true); + } + + const [assets, accounts] = await Promise.all([ + backgroundApiProxy.serviceStaking.getAvailableAssetsV2(), + backgroundApiProxy.serviceStaking.getEarnAvailableAccountsParams({ + accountId: account?.id ?? '', + networkId: allNetworkId, + indexedAccountId: account?.indexedAccountId || indexedAccount?.id, + }), + ]); + + const accountAssetPairs = accounts.flatMap((accountItem) => + assets + .filter((asset) => asset.networkId === accountItem.networkId) + .map((asset) => ({ + isAirdrop: asset.type === 'airdrop', + params: { + accountAddress: accountItem.accountAddress, + networkId: accountItem.networkId, + provider: asset.provider, + symbol: asset.symbol, + ...(asset.vault && { vault: asset.vault }), + ...(accountItem.publicKey && { + publicKey: accountItem.publicKey, + }), + }, + })), + ); + + // Filter pairs directly + const pairsWithType = options + ? accountAssetPairs.filter((pair) => { + const { params, isAirdrop } = pair; + if (options.provider && params.provider !== options.provider) + return false; + if (options.networkId && params.networkId !== options.networkId) + return false; + // For symbol filtering: + // - Normal assets: match against options.symbol (staked token symbol) + // - Airdrop assets: match against options.rewardSymbol (reward token symbol) + if (options.symbol) { + if (isAirdrop) { + // For airdrop, check if rewardSymbol is provided and matches + if ( + options.rewardSymbol && + params.symbol !== options.rewardSymbol + ) { + return false; + } + // If no rewardSymbol provided, skip symbol check for airdrops + // This allows refreshing all airdrops for the provider + } else if (params.symbol !== options.symbol) { + // For normal assets, match symbol directly + return false; + } + } + return true; + }) + : accountAssetPairs; + + // Track which keys we fetched in this round + const fetchedKeys = new Set(); + // Collect new data in this refresh batch + const batchMap = new Map(); + + // RAF throttling for batch updates + let rafId: number | null = null; + let pendingUpdate = false; + + const scheduleUpdate = () => { + if (pendingUpdate) return; + pendingUpdate = true; + + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + + rafId = requestAnimationFrame(() => { + if (isRequestStale(requestId)) return; + + const updatedMap = new Map(investmentMapRef.current); + batchMap.forEach((value, batchKey) => { + updatedMap.set(batchKey, value); + }); + updateInvestments(updatedMap); + pendingUpdate = false; + rafId = null; + }); + }; + + // Create fetch promises + const fetchPromises = pairsWithType.map(({ params, isAirdrop }) => { + const key = createInvestmentKey(params); + fetchedKeys.add(key); + return fetchInvestmentDetail(params, isAirdrop, requestId); + }); + + // Process results incrementally + const processResult = ( + result: Awaited>, + ) => { + if (!result || isRequestStale(requestId)) return; + + const { key, investment } = result; + + // Merge with existing data for the same key + const existing = batchMap.get(key); + if (existing) { + batchMap.set(key, { + ...existing, + assets: [...existing.assets, ...investment.assets], + airdropAssets: [ + ...existing.airdropAssets, + ...investment.airdropAssets, + ], + totalFiatValue: new BigNumber(existing.totalFiatValue || '0') + .plus(new BigNumber(investment.totalFiatValue || '0')) + .toFixed(), + }); + } else { + batchMap.set(key, investment); + } + + // Schedule batched update via RAF + scheduleUpdate(); + }; + + // Process each result as it arrives + for (const promise of fetchPromises) { + void promise.then(processResult); + } + + await Promise.allSettled(fetchPromises); + + // Cancel any pending RAF before final update + if (rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + + if (!isRequestStale(requestId)) { + // After all fetches complete, apply final updates and remove stale keys + const finalMap = new Map(investmentMapRef.current); + + // Apply all batch data to final map + batchMap.forEach((value, key) => { + finalMap.set(key, value); + }); + + // Only remove stale keys if this is a full refresh (no filter options) + // If options are provided, we're doing a partial refresh and shouldn't delete other data + if (!options) { + Array.from(finalMap.keys()).forEach((key) => { + if (!fetchedKeys.has(key)) { + finalMap.delete(key); + } + }); + } + + updateInvestments(finalMap); + // Only clear loading state for full refresh + if (!isPartialRefresh) { + setIsLoading(false); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + account, + indexedAccount, + allNetworkId, + getCurrentRequestId, + fetchInvestmentDetail, + updateInvestments, + isRequestStale, + hasAccountChanged, + markAccountChange, + // investmentMapRef is a ref, doesn't need to be in deps + ], + ); + + usePromiseResult( + fetchAndUpdateInvestments, + [account, allNetworkId, indexedAccount, fetchAndUpdateInvestments], + { + watchLoading: true, + revalidateOnReconnect: true, + alwaysSetState: true, + }, + ); + + const refresh = useCallback( + async (options?: IRefreshOptions) => { + await fetchAndUpdateInvestments(options); + }, + [fetchAndUpdateInvestments], + ); + + const aggregatedInvestments = useMemo( + () => aggregateByProtocol(investments), + [investments], + ); + + return { + investments: aggregatedInvestments, + earnTotalFiatValue, + isLoading, + refresh, + }; +}; diff --git a/packages/kit/src/views/Earn/hooks/useFAQListInfo.ts b/packages/kit/src/views/Earn/hooks/useFAQListInfo.ts new file mode 100644 index 000000000000..e16029a56685 --- /dev/null +++ b/packages/kit/src/views/Earn/hooks/useFAQListInfo.ts @@ -0,0 +1,25 @@ +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; + +import { usePromiseResult } from '../../../hooks/usePromiseResult'; + +export const useFAQListInfo = () => { + const { + result: faqList, + isLoading: isFaqLoading, + run: refetchFAQ, + } = usePromiseResult( + async () => { + const result = + await backgroundApiProxy.serviceStaking.getFAQListForHome(); + return result; + }, + [], + { + initResult: [], + watchLoading: true, + revalidateOnFocus: true, + }, + ); + + return { faqList, isFaqLoading, refetchFAQ }; +}; diff --git a/packages/kit/src/views/Earn/hooks/usePortfolioAction.ts b/packages/kit/src/views/Earn/hooks/usePortfolioAction.ts new file mode 100644 index 000000000000..f3af614d55a9 --- /dev/null +++ b/packages/kit/src/views/Earn/hooks/usePortfolioAction.ts @@ -0,0 +1,280 @@ +import { useCallback, useMemo, useState } from 'react'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import earnUtils from '@onekeyhq/shared/src/utils/earnUtils'; +import { EEarnLabels } from '@onekeyhq/shared/types/staking'; +import type { + IEarnActionIcon, + IEarnClaimActionIcon, + IEarnClaimWithKycActionIcon, + IEarnToken, +} from '@onekeyhq/shared/types/staking'; + +import { showClaimWithKycDialog } from '../../Staking/components/ProtocolDetails/showKYCDialog'; +import { useEarnSignMessage } from '../../Staking/hooks/useEarnSignMessage'; +import { useHandleClaim } from '../../Staking/pages/ProtocolDetails/useHandleClaim'; + +interface IUsePortfolioActionParams { + accountId: string; + networkId: string; + indexedAccountId?: string; + symbol: string; + provider: string; + vault?: string; + providerLogoURI?: string; + stakeTag?: string; +} + +export const usePortfolioAction = ({ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, + providerLogoURI, + stakeTag, +}: IUsePortfolioActionParams) => { + const [loading, setLoading] = useState(false); + + // Get earnAccount to use the correct accountId for claim + const { result: earnAccount } = usePromiseResult(async () => { + if (!accountId) { + return null; + } + return backgroundApiProxy.serviceStaking.getEarnAccount({ + accountId, + networkId, + indexedAccountId, + }); + }, [accountId, networkId, indexedAccountId]); + + const earnAccountId = useMemo( + () => earnAccount?.accountId || accountId, + [earnAccount, accountId], + ); + + const handleClaim = useHandleClaim({ accountId: earnAccountId, networkId }); + const signMessage = useEarnSignMessage(); + + const handleListaCheckAction = useCallback( + async (token?: IEarnToken) => { + setLoading(true); + await signMessage({ + accountId: earnAccountId, + networkId, + provider, + symbol: token?.symbol, + request: { origin: 'https://lista.org/', scope: 'ethereum' }, + }).finally(() => setLoading(false)); + }, + [signMessage, earnAccountId, networkId, provider], + ); + + const handleClaimAction = useCallback( + async ({ + actionIcon, + token, + rewardTokenAddress, + stakedSymbol, + rewardSymbol, + }: { + actionIcon: IEarnClaimActionIcon; + token?: IEarnToken; + rewardTokenAddress?: string; + stakedSymbol?: string; + rewardSymbol?: string; + }) => { + setLoading(true); + setTimeout(() => { + setLoading(false); + }, 10 * 1000); + + const claimAmount = actionIcon.data?.balance || '0'; + const isMorphoClaim = earnUtils.isMorphoProvider({ + providerName: provider, + }); + + const receiveToken = earnUtils.convertEarnTokenToIToken(token); + + // Use rewardTokenAddress if provided (from airdrop asset), otherwise use token.address + // Only pass claimTokenAddress if it's a non-empty string + const claimTokenAddress = + rewardTokenAddress || token?.address || undefined; + + await handleClaim({ + claimType: actionIcon.type, + symbol, + protocolInfo: { + symbol, + provider, + vault: vault || '', + networkId, + stakeTag: stakeTag || '', + providerDetail: { + name: provider, + logoURI: providerLogoURI || '', + }, + claimable: claimAmount, + }, + tokenInfo: token + ? { + balanceParsed: '0', + token, + price: '0', + networkId, + provider, + vault, + accountId, + } + : undefined, + claimAmount, + claimTokenAddress, + isMorphoClaim, + stakingInfo: { + label: EEarnLabels.Claim, + protocol: earnUtils.getEarnProviderName({ providerName: provider }), + protocolLogoURI: providerLogoURI || '', + receive: receiveToken + ? { token: receiveToken, amount: claimAmount } + : undefined, + tags: stakeTag ? [stakeTag] : [], + }, + // For airdrops, use stakedSymbol to refresh the correct portfolio item + // For normal claims, use token?.symbol + portfolioSymbol: stakedSymbol || token?.symbol, + // For airdrops, also pass rewardSymbol to filter the correct airdrop asset + portfolioRewardSymbol: rewardSymbol, + }); + setLoading(false); + }, + [ + handleClaim, + provider, + symbol, + vault, + networkId, + stakeTag, + providerLogoURI, + accountId, + ], + ); + + const handleClaimWithKycAction = useCallback( + async ({ + actionIcon, + indexedAccountId: actionIndexedAccountId, + }: { + actionIcon: IEarnClaimWithKycActionIcon; + indexedAccountId?: string; + }) => { + setLoading(true); + try { + // Get fresh data from API + const response = + await backgroundApiProxy.serviceStaking.getProtocolDetailsV2({ + accountId, + networkId, + indexedAccountId: actionIndexedAccountId ?? indexedAccountId, + symbol, + provider, + vault, + }); + + // Find the updated action in portfolios + const buttons = + response?.portfolios?.items + ?.flatMap((item) => item.buttons || []) + .filter((button) => 'type' in button) || []; + + const latestClaimWithKycAction = buttons.find( + (button) => button.type === 'claimWithKyc', + ) as IEarnClaimWithKycActionIcon | undefined; + + const latestClaimAction = !latestClaimWithKycAction + ? (buttons.find((button) => button.type === 'claim') as + | IEarnClaimActionIcon + | undefined) + : undefined; + + // Priority: claimWithKyc > claim > no response + if (latestClaimWithKycAction) { + showClaimWithKycDialog({ + actionData: latestClaimWithKycAction, + }); + } else if (latestClaimAction) { + await handleClaimAction({ + actionIcon: latestClaimAction, + token: actionIcon.data?.token, + }); + } else { + showClaimWithKycDialog({ + actionData: actionIcon, + }); + } + } catch (error) { + showClaimWithKycDialog({ + actionData: actionIcon, + }); + } finally { + setLoading(false); + } + }, + [ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, + handleClaimAction, + ], + ); + + const handleAction = useCallback( + ({ + actionIcon, + token, + rewardTokenAddress, + indexedAccountId: actionIndexedAccountId, + stakedSymbol, + }: { + actionIcon: IEarnActionIcon; + token?: IEarnToken; + rewardTokenAddress?: string; + indexedAccountId?: string; + stakedSymbol?: string; + }) => { + switch (actionIcon.type) { + case 'claim': + case 'claimOrder': + case 'claimAirdrop': + void handleClaimAction({ + actionIcon, + token, + rewardTokenAddress, + stakedSymbol, + }); + break; + case 'claimWithKyc': + void handleClaimWithKycAction({ + actionIcon: actionIcon as IEarnClaimWithKycActionIcon, + indexedAccountId: actionIndexedAccountId, + }); + break; + case 'listaCheck': + void handleListaCheckAction(token); + break; + default: + break; + } + }, + [handleClaimAction, handleClaimWithKycAction, handleListaCheckAction], + ); + + return { + loading, + handleAction, + }; +}; diff --git a/packages/kit/src/views/Earn/hooks/useToTokenProviderListPage.ts b/packages/kit/src/views/Earn/hooks/useToTokenProviderListPage.ts new file mode 100644 index 000000000000..99790761b3d9 --- /dev/null +++ b/packages/kit/src/views/Earn/hooks/useToTokenProviderListPage.ts @@ -0,0 +1,24 @@ +import { useCallback } from 'react'; + +import type { IEarnAvailableAssetProtocol } from '@onekeyhq/shared/types/earn'; + +import useAppNavigation from '../../../hooks/useAppNavigation'; +import { EarnNavigation } from '../earnUtils'; + +export function useToTokenProviderListPage() { + const navigation = useAppNavigation(); + + return useCallback( + async (params: { + networkId: string; + accountId: string; + indexedAccountId?: string; + symbol: string; + protocols: IEarnAvailableAssetProtocol[]; + logoURI?: string; + }) => { + await EarnNavigation.toTokenProviderListPage(navigation, params); + }, + [navigation], + ); +} diff --git a/packages/kit/src/views/Earn/pages/EarnProtocolDetails/components/ApyChart.tsx b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/components/ApyChart.tsx new file mode 100644 index 000000000000..7bda5e37f7ee --- /dev/null +++ b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/components/ApyChart.tsx @@ -0,0 +1,328 @@ +import { useCallback, useMemo, useState } from 'react'; + +import { useIntl } from 'react-intl'; + +import { + Icon, + IconButton, + SizableText, + Skeleton, + XStack, + YStack, + useMedia, + useShare, +} from '@onekeyhq/components'; +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { LightweightChart } from '@onekeyhq/kit/src/components/LightweightChart'; +import { Token } from '@onekeyhq/kit/src/components/Token'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import { EarnActionIcon } from '@onekeyhq/kit/src/views/Staking/components/ProtocolDetails/EarnActionIcon'; +import { EarnText } from '@onekeyhq/kit/src/views/Staking/components/ProtocolDetails/EarnText'; +import { useDevSettingsPersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { ETabEarnRoutes } from '@onekeyhq/shared/src/routes'; +import type { + IEarnTokenInfo, + IStakeEarnDetail, +} from '@onekeyhq/shared/types/staking'; + +import { EarnNavigation } from '../../../earnUtils'; + +import type { UTCTimestamp } from 'lightweight-charts'; + +interface IApyChartProps { + networkId: string; + symbol: string; + provider: string; + vault?: string; + apyDetail: IStakeEarnDetail['apyDetail']; + tokenInfo?: IEarnTokenInfo; +} + +export function ApyChart({ + networkId, + symbol, + provider, + vault, + apyDetail, + tokenInfo, +}: IApyChartProps) { + const intl = useIntl(); + const { shareText } = useShare(); + const { gtMd } = useMedia(); + const [devSettings] = useDevSettingsPersistAtom(); + const navigation = useAppNavigation(); + + // Generate share URL + const shareUrl = useMemo(() => { + if (!symbol || !provider || !networkId) return undefined; + const shareLink = EarnNavigation.generateEarnShareLink({ + networkId, + symbol, + provider, + vault, + isDevMode: devSettings.enabled, + }); + return shareLink; + }, [symbol, provider, networkId, vault, devSettings.enabled]); + + const handleShare = useCallback(() => { + if (!shareUrl) return; + void shareText(shareUrl); + }, [shareUrl, shareText]); + + const handleMyPortfolio = useCallback(() => { + navigation.navigate(ETabEarnRoutes.EarnHome, { tab: 'portfolio' }); + }, [navigation]); + + // Hover state for popover + const [hoverData, setHoverData] = useState<{ + time: number; + apy: number; + x: number; + y: number; + } | null>(null); + + const [containerWidth, setContainerWidth] = useState(0); + + const handleHover = useCallback( + ({ + time, + price, + x, + y, + }: { + time?: number; + price?: number; + x?: number; + y?: number; + }) => { + if (time && price && x !== undefined && y !== undefined) { + setHoverData({ + time, + apy: price, + x, + y, + }); + } else { + setHoverData(null); + } + }, + [], + ); + + // Calculate popover position - switch side at midpoint + const popoverPosition = useMemo(() => { + if (!hoverData || !containerWidth) return null; + + const POPOVER_WIDTH = 120; + const OFFSET = 10; // Distance from cursor + const isLeftHalf = hoverData.x < containerWidth / 2; + + return { + left: isLeftHalf ? hoverData.x + OFFSET : hoverData.x - OFFSET, + translateXValue: isLeftHalf ? 0 : -POPOVER_WIDTH, // Left align or right align + top: Math.max(10, hoverData.y - 70), + }; + }, [hoverData, containerWidth]); + + // Format date for popover with i18n + const formatPopoverDate = useCallback( + (timestamp: number) => { + const date = new Date(timestamp * 1000); + return intl.formatDate(date, { + month: 'short', + day: '2-digit', + year: 'numeric', + }); + }, + [intl], + ); + + const { result: chartData, isLoading } = usePromiseResult( + async () => { + const apyHistory = await backgroundApiProxy.serviceStaking.getApyHistory({ + networkId, + symbol, + provider, + vault, + }); + + if (!apyHistory || apyHistory.length === 0) { + return null; + } + + // Calculate high and low APY + const apyValues = apyHistory.map((item) => Number(item.apy)); + const high = Math.max(...apyValues); + const low = Math.min(...apyValues); + + // Convert to chart format + // timestamp is in milliseconds, need to convert to seconds for UTCTimestamp + const formattedData = apyHistory + .map((item) => ({ + time: Math.floor(item.timestamp / 1000) as UTCTimestamp, + value: Number(item.apy), + })) + .sort((a, b) => a.time - b.time); + + // Convert to Market chart format [timestamp, value][] + const marketChartData = formattedData.map( + (item) => [item.time, item.value] as [UTCTimestamp, number], + ); + + return { + high, + low, + marketChartData, + }; + }, + [networkId, symbol, provider, vault], + { watchLoading: true }, + ); + + if (isLoading) { + return ( + + + {/* Token icon and name skeleton */} + + + + + {/* APY value skeleton */} + + {/* High and Low skeleton */} + + + + + + + + + + + + {/* Chart skeleton */} + + + ); + } + + return ( + + + {/* Token icon and name with My Portfolio button - always show */} + + + + + {tokenInfo?.token.symbol || symbol} + + + + + {intl.formatMessage({ id: ETranslations.earn_portfolio })} + + + + + {/* APY value with buttons - only show if apyDetail exists */} + {apyDetail ? ( + <> + + + + + + {/* High and Low values */} + {gtMd && chartData ? ( + + + + {intl.formatMessage({ id: ETranslations.market_high })} + + + {chartData.high.toFixed(2)}% + + + + + {intl.formatMessage({ id: ETranslations.market_low })} + + + {chartData.low.toFixed(2)}% + + + + ) : null} + + ) : null} + + {chartData ? ( + { + const width = e.nativeEvent.layout.width; + if (width !== containerWidth) { + setContainerWidth(width); + } + }} + > + {/* Hover Popover - follows cursor/touch position with boundary detection */} + {hoverData && popoverPosition ? ( + + + + {hoverData.apy.toFixed(2)}% + + + {formatPopoverDate(hoverData.time)} + + + + ) : null} + + + ) : null} + + ); +} diff --git a/packages/kit/src/views/Earn/pages/EarnProtocolDetails/hooks/useProtocolDetailBreadcrumb.ts b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/hooks/useProtocolDetailBreadcrumb.ts new file mode 100644 index 000000000000..113654ec6172 --- /dev/null +++ b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/hooks/useProtocolDetailBreadcrumb.ts @@ -0,0 +1,101 @@ +import { useMemo } from 'react'; + +import { useIntl } from 'react-intl'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { ETabEarnRoutes, ETabRoutes } from '@onekeyhq/shared/src/routes'; +import type { ISupportedSymbol } from '@onekeyhq/shared/types/earn'; +import { normalizeToEarnProvider } from '@onekeyhq/shared/types/earn/earnProvider.constants'; +import type { IEarnTokenInfo } from '@onekeyhq/shared/types/staking'; + +interface IUseProtocolDetailBreadcrumbParams { + accountId?: string; + indexedAccountId?: string; + symbol: ISupportedSymbol; + provider: string; + tokenInfo?: IEarnTokenInfo; +} + +export function useProtocolDetailBreadcrumb({ + accountId, + indexedAccountId, + symbol, + provider, + tokenInfo, +}: IUseProtocolDetailBreadcrumbParams) { + const intl = useIntl(); + const appNavigation = useAppNavigation(); + + // Fetch protocol list to determine if there are multiple protocols for this token + const { result: protocolList } = usePromiseResult(async () => { + if (!accountId || !symbol) { + return []; + } + + try { + const data = await backgroundApiProxy.serviceStaking.getProtocolList({ + symbol, + accountId, + indexedAccountId, + }); + return data || []; + } catch (error) { + return []; + } + }, [accountId, indexedAccountId, symbol]); + + const hasMultipleProtocols = useMemo( + () => (protocolList?.length ?? 0) > 1, + [protocolList], + ); + + const breadcrumbProps = useMemo(() => { + const items: Array<{ label: string; onClick?: () => void }> = [ + { + label: intl.formatMessage({ id: ETranslations.global_earn }), + onClick: () => { + appNavigation.switchTab(ETabRoutes.Earn, { + screen: ETabEarnRoutes.EarnHome, + }); + }, + }, + ]; + + // If there are multiple protocols, add a middle breadcrumb to protocol list + if (hasMultipleProtocols && tokenInfo?.token?.logoURI) { + items.push({ + label: symbol, + onClick: () => { + appNavigation.push(ETabEarnRoutes.EarnProtocols, { + symbol, + logoURI: encodeURIComponent(tokenInfo.token.logoURI), + }); + }, + }); + items.push({ + label: normalizeToEarnProvider(provider) || provider, + }); + } else { + // Only two levels: Earn > Symbol + items.push({ label: symbol }); + } + + return { items }; + }, [ + intl, + symbol, + provider, + appNavigation, + hasMultipleProtocols, + tokenInfo?.token?.logoURI, + ]); + + return { + breadcrumbProps, + hasMultipleProtocols, + protocolList, + }; +} diff --git a/packages/kit/src/views/Earn/pages/EarnProtocolDetails/hooks/useProtocolDetailData.ts b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/hooks/useProtocolDetailData.ts new file mode 100644 index 000000000000..e827f1280619 --- /dev/null +++ b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/hooks/useProtocolDetailData.ts @@ -0,0 +1,125 @@ +import { useMemo } from 'react'; + +import BigNumber from 'bignumber.js'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import { buildLocalTxStatusSyncId } from '@onekeyhq/kit/src/views/Staking/utils/utils'; +import type { ISupportedSymbol } from '@onekeyhq/shared/types/earn'; +import type { + IEarnTokenInfo, + IEarnWithdrawActionIcon, + IProtocolInfo, +} from '@onekeyhq/shared/types/staking'; + +export function useProtocolDetailData({ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, +}: { + accountId: string; + networkId: string; + indexedAccountId: string | undefined; + symbol: ISupportedSymbol; + provider: string; + vault: string | undefined; +}) { + const { + result: earnAccount, + run: refreshAccount, + isLoading: isAccountLoading, + } = usePromiseResult( + async () => + backgroundApiProxy.serviceStaking.getEarnAccount({ + accountId, + networkId, + indexedAccountId, + btcOnlyTaproot: true, + }), + [accountId, indexedAccountId, networkId], + { watchLoading: true }, + ); + + const { + result: detailInfo, + isLoading: isDetailLoading, + run, + } = usePromiseResult( + async () => { + const response = + await backgroundApiProxy.serviceStaking.getProtocolDetailsV2({ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, + }); + + return response; + }, + [accountId, networkId, indexedAccountId, symbol, provider, vault], + { watchLoading: true }, + ); + + const tokenInfo = useMemo(() => { + if (detailInfo?.subscriptionValue?.token) { + const balanceBN = new BigNumber( + detailInfo.subscriptionValue.balance || '0', + ); + const balanceParsed = balanceBN.isNaN() ? '0' : balanceBN.toFixed(); + + return { + balanceParsed, + token: detailInfo.subscriptionValue.token.info, + price: detailInfo.subscriptionValue.token.price, + networkId, + provider, + vault, + accountId: accountId ?? '', + }; + } + return undefined; + }, [detailInfo, networkId, provider, vault, accountId]); + + const protocolInfo = useMemo(() => { + if (!detailInfo?.protocol || !earnAccount) { + return undefined; + } + + const withdrawAction = detailInfo?.actions?.find( + (i) => i.type === 'withdraw', + ) as IEarnWithdrawActionIcon; + + return { + ...detailInfo.protocol, + apyDetail: detailInfo.apyDetail, + earnAccount, + activeBalance: withdrawAction?.data?.balance, + eventEndTime: detailInfo?.countDownAlert?.endTime, + stakeTag: buildLocalTxStatusSyncId({ + providerName: provider, + tokenSymbol: symbol, + }), + overflowBalance: detailInfo.nums?.overflow, + maxUnstakeAmount: detailInfo.nums?.maxUnstakeAmount, + minUnstakeAmount: detailInfo.nums?.minUnstakeAmount, + minTransactionFee: detailInfo.nums?.minTransactionFee, + remainingCap: detailInfo.nums?.remainingCap, + claimable: detailInfo.nums?.claimable, + }; + }, [detailInfo, earnAccount, provider, symbol]); + + return { + earnAccount, + detailInfo, + tokenInfo, + protocolInfo, + isLoading: isAccountLoading || isDetailLoading, + refreshData: run, + refreshAccount, + }; +} diff --git a/packages/kit/src/views/Earn/pages/EarnProtocolDetails/index.tsx b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/index.tsx new file mode 100644 index 000000000000..010c13a762ec --- /dev/null +++ b/packages/kit/src/views/Earn/pages/EarnProtocolDetails/index.tsx @@ -0,0 +1,777 @@ +import { Fragment, useCallback, useMemo, useState } from 'react'; + +import { useIntl } from 'react-intl'; +import { useSharedValue } from 'react-native-reanimated'; + +import { + Button, + Divider, + Image, + Page, + SizableText, + Skeleton, + Stack, + Tabs, + XStack, + YStack, + useMedia, +} from '@onekeyhq/components'; +import type backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { AccountSelectorProviderMirror } from '@onekeyhq/kit/src/components/AccountSelector'; +import { CountDownCalendarAlert } from '@onekeyhq/kit/src/components/CountDownCalendarAlert'; +import { Token } from '@onekeyhq/kit/src/components/Token'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { useAppRoute } from '@onekeyhq/kit/src/hooks/useAppRoute'; +import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accountSelector'; +import { EJotaiContextStoreNames } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import type { + ETabEarnRoutes, + ITabEarnParamList, +} from '@onekeyhq/shared/src/routes'; +import { + EModalReceiveRoutes, + EModalRoutes, + EModalStakingRoutes, + ETabRoutes, +} from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import type { ISupportedSymbol } from '@onekeyhq/shared/types/earn'; +import { + normalizeToEarnProvider, + normalizeToEarnSymbol, +} from '@onekeyhq/shared/types/earn/earnProvider.constants'; +import { EStakingActionType } from '@onekeyhq/shared/types/staking'; +import type { + IEarnAlert, + IEarnTokenInfo, + IStakeEarnDetail, +} from '@onekeyhq/shared/types/staking'; + +import { DiscoveryBrowserProviderMirror } from '../../../Discovery/components/DiscoveryBrowserProviderMirror'; +import { + PageFrame, + isErrorState, + isLoadingState, +} from '../../../Staking/components/PageFrame'; +import { EarnActionIcon } from '../../../Staking/components/ProtocolDetails/EarnActionIcon'; +import { EarnAlert } from '../../../Staking/components/ProtocolDetails/EarnAlert'; +import { EarnIcon } from '../../../Staking/components/ProtocolDetails/EarnIcon'; +import { EarnText } from '../../../Staking/components/ProtocolDetails/EarnText'; +import { GridItem } from '../../../Staking/components/ProtocolDetails/GridItemV2'; +import { NoAddressWarning } from '../../../Staking/components/ProtocolDetails/NoAddressWarning'; +import { PeriodSection } from '../../../Staking/components/ProtocolDetails/PeriodSectionV2'; +import { ProtectionSection } from '../../../Staking/components/ProtocolDetails/ProtectionSectionV2'; +import { OverviewSkeleton } from '../../../Staking/components/StakingSkeleton'; +import { useCheckEthenaKycStatus } from '../../../Staking/hooks/useCheckEthenaKycStatus'; +import { useHandleSwap } from '../../../Staking/hooks/useHandleSwap'; +import { useUnsupportedProtocol } from '../../../Staking/hooks/useUnsupportedProtocol'; +import { ManagePositionContent } from '../../../Staking/pages/ManagePosition/components/ManagePositionContent'; +import { FAQSection } from '../../../Staking/pages/ProtocolDetailsV2/FAQSection'; +import { EarnPageContainer } from '../../components/EarnPageContainer'; +import { EarnProviderMirror } from '../../EarnProviderMirror'; +import { EarnNetworkUtils } from '../../earnUtils'; + +import { ApyChart } from './components/ApyChart'; +import { useProtocolDetailBreadcrumb } from './hooks/useProtocolDetailBreadcrumb'; +import { useProtocolDetailData } from './hooks/useProtocolDetailData'; + +function ManagersSection({ + managers, + noPadding, +}: { + managers: IStakeEarnDetail['managers'] | undefined; + noPadding?: boolean; +}) { + return managers?.items?.length ? ( + + {managers.items.map((item, index) => ( + + + + + + + {index !== managers.items.length - 1 ? ( + + + + ) : null} + + ))} + + ) : null; +} + +function ChartSection({ + networkId, + symbol, + provider, + vault, + apyDetail, + tokenInfo, +}: { + networkId: string; + symbol: string; + provider: string; + vault?: string; + apyDetail: IStakeEarnDetail['apyDetail']; + tokenInfo?: IEarnTokenInfo; +}) { + return ( + + ); +} + +function IntroSection({ intro }: { intro?: IStakeEarnDetail['intro'] }) { + if (!intro) { + return null; + } + + return ( + <> + {intro.items?.length ? ( + + + + {intro.items.map((cell) => ( + + {(cell?.items ?? []).map((item) => ( + + + + + ))} + + ) : null + } + actionIcon={cell.button} + tooltip={cell.tooltip} + type={cell.type} + /> + ))} + + + ) : null} + + + ); +} + +function AlertSection({ alerts }: { alerts?: IEarnAlert[] }) { + return ; +} + +function ProviderSection({ + provider, +}: { + provider: IStakeEarnDetail['provider']; +}) { + return provider ? ( + <> + + + + {provider.items.map((cell) => ( + + ))} + + + + + ) : null; +} + +function PerformanceSection({ + performance, +}: { + performance?: IStakeEarnDetail['intro']; +}) { + if (!performance) { + return null; + } + + return ( + <> + {performance.items?.length ? ( + + + + {performance.items.map((cell) => ( + + {(cell?.items ?? []).map((item) => ( + + + + + ))} + + ) : null + } + actionIcon={cell.button} + tooltip={cell.tooltip} + type={cell.type} + /> + ))} + + + ) : null} + + + ); +} + +function RiskSection({ risk }: { risk?: IStakeEarnDetail['risk'] }) { + return risk ? ( + <> + + + + {risk.items?.map((item) => ( + <> + + + + + + + + + + + + + + {item.list?.length ? ( + + {item.list.map((i, indexOfList) => ( + + + + + ))} + + ) : null} + + ))} + + + + + ) : null; +} + +const DetailsPart = ({ + detailInfo, + tokenInfo, + isLoading, + keepSkeletonVisible, + onRefresh, + networkId, + symbol, + provider, + vault, +}: { + detailInfo: IStakeEarnDetail | undefined; + tokenInfo?: IEarnTokenInfo; + isLoading: boolean; + keepSkeletonVisible: boolean; + onRefresh: () => void; + networkId: string; + symbol: string; + provider: string; + vault?: string; +}) => { + const now = useMemo(() => Date.now(), []); + const { gtMd } = useMedia(); + + return ( + + + {detailInfo ? ( + + + + + {detailInfo?.countDownAlert?.startTime && + detailInfo?.countDownAlert?.endTime && + now > detailInfo.countDownAlert.startTime && + detailInfo.countDownAlert.endTime < now ? ( + + + + ) : null} + + + + + + + ) : null} + + + ); +}; + +const ManagePositionPart = ({ + networkId, + symbol, + provider, + vault, + onCreateAddress, + onStakeWithdrawSuccess, +}: { + networkId: string; + symbol: string; + provider: string; + vault?: string; + onCreateAddress?: () => Promise; + onStakeWithdrawSuccess?: () => void; +}) => { + const { activeAccount } = useActiveAccount({ num: 0 }); + const { account, indexedAccount } = activeAccount; + + return ( + + + + + + ); +}; + +const EarnProtocolDetailsPage = () => { + const route = useAppRoute< + ITabEarnParamList, + ETabEarnRoutes.EarnProtocolDetails | ETabEarnRoutes.EarnProtocolDetailsShare + >(); + const intl = useIntl(); + const appNavigation = useAppNavigation(); + const { gtMd } = useMedia(); + const { activeAccount } = useActiveAccount({ num: 0 }); + const { account, indexedAccount } = activeAccount; + const [stakeLoading, setStakeLoading] = useState(false); + const [keepSkeletonVisible, setKeepSkeletonVisible] = useState(false); + + // Parse route params, support both normal and share link routes + const resolvedParams = useMemo<{ + accountId: string; + indexedAccountId: string | undefined; + networkId: string; + symbol: ISupportedSymbol; + provider: string; + vault: string | undefined; + isFromShareLink: boolean; + }>(() => { + const routeParams = route.params as any; + + // Check if it is the new share link format + if ('network' in routeParams) { + // New format: /earn/:network/:symbol/:provider + const { + network, + symbol: symbolParam, + provider: providerParam, + vault, + } = routeParams; + const networkId = EarnNetworkUtils.getNetworkIdByName(network); + const symbol = normalizeToEarnSymbol(symbolParam); + const provider = normalizeToEarnProvider(providerParam); + + if (!networkId) { + throw new OneKeyLocalError(`Unknown network: ${String(network)}`); + } + if (!symbol) { + throw new OneKeyLocalError(`Unknown symbol: ${String(symbolParam)}`); + } + if (!provider) { + throw new OneKeyLocalError( + `Unknown provider: ${String(providerParam)}`, + ); + } + + return { + accountId: activeAccount.account?.id || '', + indexedAccountId: activeAccount.indexedAccount?.id, + networkId, + symbol, + provider, + vault, + isFromShareLink: true, + }; + } + + // Old format: normal navigation + const { + accountId: routeAccountId, + indexedAccountId: routeIndexedAccountId, + networkId, + symbol, + provider, + vault, + } = routeParams; + + return { + accountId: routeAccountId || activeAccount.account?.id || '', + indexedAccountId: + routeIndexedAccountId || activeAccount.indexedAccount?.id, + networkId, + symbol, + provider, + vault, + isFromShareLink: false, + }; + }, [route.params, activeAccount]); + + const { accountId, networkId, indexedAccountId, symbol, provider, vault } = + resolvedParams; + + const { + earnAccount, + detailInfo, + tokenInfo, + protocolInfo, + isLoading, + refreshData, + refreshAccount, + } = useProtocolDetailData({ + accountId, + networkId, + indexedAccountId: indexedAccount?.id, + symbol, + provider, + vault, + }); + + useUnsupportedProtocol({ + detailInfo, + appNavigation, + setKeepSkeletonVisible, + }); + + useCheckEthenaKycStatus({ + provider, + refreshEarnDetailData: refreshData, + }); + + const hasNoAccount = !account?.id && !indexedAccount?.id; + const hasNoAddress = !earnAccount?.accountAddress; + + const onCreateAddress = useCallback(async () => { + await refreshAccount(); + await refreshData(); + }, [refreshAccount, refreshData]); + + const handleStakeWithdrawSuccess = useCallback(() => { + void refreshData(); + }, [refreshData]); + + // Use custom hook for breadcrumb management + const { breadcrumbProps } = useProtocolDetailBreadcrumb({ + accountId: account?.id, + indexedAccountId: indexedAccount?.id, + symbol, + provider, + tokenInfo, + }); + + const pageTitle = useMemo( + () => ( + + + {symbol} + + + ), + [symbol], + ); + + const handleNavigateToManagePosition = useCallback( + (tab?: 'deposit' | 'withdraw') => { + appNavigation.pushModal(EModalRoutes.StakingModal, { + screen: EModalStakingRoutes.ManagePosition, + params: { + networkId, + symbol, + provider, + vault, + tab, + }, + }); + }, + [appNavigation, networkId, symbol, provider, vault], + ); + + const { handleSwap } = useHandleSwap(); + + const handleReceiveUSDe = useCallback(() => { + if (!detailInfo?.subscriptionValue?.token?.info || !earnAccount) return; + appNavigation.pushModal(EModalRoutes.ReceiveModal, { + screen: EModalReceiveRoutes.ReceiveToken, + params: { + networkId, + accountId: earnAccount.accountId, + walletId: earnAccount.walletId, + token: detailInfo.subscriptionValue.token.info, + }, + }); + }, [ + appNavigation, + networkId, + earnAccount, + detailInfo?.subscriptionValue?.token?.info, + ]); + + const handleTradeUSDe = useCallback(async () => { + if (!detailInfo?.subscriptionValue?.token?.info) return; + await handleSwap({ + token: detailInfo.subscriptionValue.token.info, + networkId, + }); + }, [handleSwap, networkId, detailInfo?.subscriptionValue?.token?.info]); + + const pageFooter = useMemo(() => { + if (gtMd) { + return null; + } + + const shouldDisableButtons = isLoading || hasNoAccount || hasNoAddress; + + // USDe: show Receive/Trade buttons + if (symbol === 'USDe') { + const receiveAction = detailInfo?.actions?.find( + (a) => a.type === EStakingActionType.Receive, + ); + const tradeAction = detailInfo?.actions?.find( + (a) => a.type === EStakingActionType.Trade, + ); + + if (!receiveAction && !tradeAction) { + return null; + } + + return ( + + ); + } + + // Normal assets: show Deposit/Withdraw buttons + return ( + handleNavigateToManagePosition('withdraw'), + disabled: shouldDisableButtons, + }} + onConfirmText={intl.formatMessage({ id: ETranslations.earn_deposit })} + confirmButtonProps={{ + variant: 'primary', + onPress: () => handleNavigateToManagePosition('deposit'), + disabled: shouldDisableButtons, + }} + /> + ); + }, [ + gtMd, + intl, + handleNavigateToManagePosition, + symbol, + detailInfo?.actions, + handleReceiveUSDe, + handleTradeUSDe, + hasNoAccount, + hasNoAddress, + isLoading, + ]); + + return ( + + + + } + footer={pageFooter} + > + + + + {!gtMd && (hasNoAccount || hasNoAddress) && !isLoading ? ( + + + + ) : null} + + {gtMd ? ( + + + + ) : null} + + + ); +}; + +function EarnProtocolDetailsPageWithProvider() { + return ( + + + + + + + + ); +} + +export default EarnProtocolDetailsPageWithProvider; diff --git a/packages/kit/src/views/Earn/pages/EarnProtocols/index.tsx b/packages/kit/src/views/Earn/pages/EarnProtocols/index.tsx new file mode 100644 index 000000000000..3895c9d27067 --- /dev/null +++ b/packages/kit/src/views/Earn/pages/EarnProtocols/index.tsx @@ -0,0 +1,358 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useIntl } from 'react-intl'; + +import { + Badge, + Empty, + Image, + SizableText, + Skeleton, + XStack, + YStack, + useMedia, +} from '@onekeyhq/components'; +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { AccountSelectorProviderMirror } from '@onekeyhq/kit/src/components/AccountSelector'; +import { ListItem } from '@onekeyhq/kit/src/components/ListItem'; +import type { ITableColumn } from '@onekeyhq/kit/src/components/ListView/TableList'; +import { TableList } from '@onekeyhq/kit/src/components/ListView/TableList'; +import { NetworkAvatarGroup } from '@onekeyhq/kit/src/components/NetworkAvatar/NetworkAvatar'; +import { Token } from '@onekeyhq/kit/src/components/Token'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accountSelector'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; +import { ETabEarnRoutes, ETabRoutes } from '@onekeyhq/shared/src/routes'; +import type { ITabEarnParamList } from '@onekeyhq/shared/src/routes'; +import earnUtils from '@onekeyhq/shared/src/utils/earnUtils'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import { normalizeToEarnProvider } from '@onekeyhq/shared/types/earn/earnProvider.constants'; +import type { IStakeProtocolListItem } from '@onekeyhq/shared/types/staking'; + +import { EarnText } from '../../../Staking/components/ProtocolDetails/EarnText'; +import { AprText } from '../../components/AprText'; +import { EarnPageContainer } from '../../components/EarnPageContainer'; + +import type { RouteProp } from '@react-navigation/core'; + +type IRouteProps = RouteProp; + +function BasicEarnProtocols({ route }: { route: IRouteProps }) { + const intl = useIntl(); + const navigation = useAppNavigation(); + const { + symbol, + filterNetworkId, + logoURI: encodedLogoURI, + } = route.params || {}; + + const logoURI = useMemo(() => { + try { + return encodedLogoURI ? decodeURIComponent(encodedLogoURI) : undefined; + } catch { + return undefined; + } + }, [encodedLogoURI]); + + const media = useMedia(); + const { activeAccount } = useActiveAccount({ num: 0 }); + + const customHeaderLeft = useMemo( + () => ( + <> + + + {symbol || + intl.formatMessage({ + id: ETranslations.earn_symbol_staking_provider, + })} + + + ), + [intl, symbol, logoURI], + ); + + const [protocolData, setProtocolData] = useState( + [], + ); + const [isLoading, setIsLoading] = useState(true); + + const fetchProtocolData = useCallback(async () => { + if (!activeAccount?.account?.id) { + return; + } + + try { + setIsLoading(true); + + const data = await backgroundApiProxy.serviceStaking.getProtocolList({ + symbol, + accountId: activeAccount.account.id, + indexedAccountId: activeAccount.indexedAccount?.id, + filterNetworkId, + }); + + // const groupedData = groupProtocolsByGroup(intl, data); + setProtocolData(data); + } catch (error) { + setProtocolData([]); + } finally { + setIsLoading(false); + } + }, [ + symbol, + activeAccount?.account?.id, + activeAccount?.indexedAccount?.id, + filterNetworkId, + ]); + + useEffect(() => { + void fetchProtocolData(); + }, [fetchProtocolData]); + + const handleProtocolPress = useCallback( + async (protocol: IStakeProtocolListItem) => { + if (!activeAccount?.account?.id) { + return; + } + + try { + defaultLogger.staking.page.selectProvider({ + network: protocol.network.networkId, + stakeProvider: protocol.provider.name, + }); + + const earnAccount = + await backgroundApiProxy.serviceStaking.getEarnAccount({ + accountId: activeAccount.account.id, + indexedAccountId: activeAccount.indexedAccount?.id, + networkId: protocol.network.networkId, + }); + + navigation.push(ETabEarnRoutes.EarnProtocolDetails, { + networkId: protocol.network.networkId, + accountId: earnAccount?.accountId || activeAccount.account.id, + indexedAccountId: + earnAccount?.account.indexedAccountId || + activeAccount.indexedAccount?.id, + symbol, + provider: protocol.provider.name, + vault: earnUtils.isVaultBasedProvider({ + providerName: protocol.provider.name, + }) + ? protocol.provider.vault + : undefined, + }); + } catch (error) { + // Handle error silently + } + }, + [ + activeAccount?.account?.id, + activeAccount?.indexedAccount?.id, + symbol, + navigation, + ], + ); + + const columns: ITableColumn[] = useMemo(() => { + return [ + { + key: 'protocol', + label: intl.formatMessage({ id: ETranslations.global_protocol }), + flex: 2.5, + render: (item) => { + return ( + + + + + + {normalizeToEarnProvider(item.provider.name)} + + {item.provider.badges?.map((badge) => ( + + {badge.tag} + + ))} + + {item?.provider?.description ? ( + + {item.provider.description} + + ) : null} + + + ); + }, + }, + { + key: 'network', + label: intl.formatMessage({ id: ETranslations.global_network }), + flex: 1, + hideInMobile: true, + render: (item) => ( + + ), + }, + { + key: 'tvl', + label: intl.formatMessage({ id: ETranslations.earn_tvl }), + flex: 1, + hideInMobile: true, + render: (item) => ( + + + + ), + }, + { + key: 'yield', + label: intl.formatMessage({ id: ETranslations.global_apr }), + flex: 1, + render: (item) => ( + + ), + }, + ]; + }, [intl]); + + const content = useMemo(() => { + if (isLoading) { + return ( + + + + + {Array.from({ length: 2 }).map((_, index) => ( + + + + + + + + + + + ))} + + ); + } + + if (protocolData.length === 0) { + return ( + + { + void fetchProtocolData(); + }, + }} + /> + + ); + } + + return ( + + data={protocolData} + columns={columns} + defaultSortKey="yield" + defaultSortDirection="desc" + onPressRow={handleProtocolPress} + enableDrillIn={media.gtSm} + isLoading={isLoading} + /> + ); + }, [ + media, + columns, + fetchProtocolData, + intl, + isLoading, + protocolData, + handleProtocolPress, + ]); + + return ( + { + navigation.switchTab(ETabRoutes.Earn, { + screen: ETabEarnRoutes.EarnHome, + }); + }, + }, + { + label: + symbol || + intl.formatMessage({ + id: ETranslations.earn_symbol_staking_provider, + }), + }, + ], + }} + showBackButton + > + {content} + + ); +} + +export default function EarnProtocols(props: { route: IRouteProps }) { + return ( + + + + ); +} diff --git a/packages/kit/src/views/Earn/utils/getNumberColor.ts b/packages/kit/src/views/Earn/utils/getNumberColor.ts new file mode 100644 index 000000000000..c64243a34d34 --- /dev/null +++ b/packages/kit/src/views/Earn/utils/getNumberColor.ts @@ -0,0 +1,9 @@ +import type { ISizableTextProps } from '@onekeyhq/components'; + +export const getNumberColor = ( + value: string | number, + defaultColor: ISizableTextProps['color'] = '$textSuccess', +): ISizableTextProps['color'] => + (typeof value === 'string' ? Number(value) : value) === 0 + ? '$text' + : defaultColor; diff --git a/packages/kit/src/views/Home/components/WalletActions/WalletActionEarn.tsx b/packages/kit/src/views/Home/components/WalletActions/WalletActionEarn.tsx index fdd512241bce..66ddc2873566 100644 --- a/packages/kit/src/views/Home/components/WalletActions/WalletActionEarn.tsx +++ b/packages/kit/src/views/Home/components/WalletActions/WalletActionEarn.tsx @@ -7,10 +7,9 @@ import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/background import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; import { useUserWalletProfile } from '@onekeyhq/kit/src/hooks/useUserWalletProfile'; -import { showProtocolListDialog } from '@onekeyhq/kit/src/views/Earn/components/showProtocolListDialog'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; -import { EModalRoutes, EModalStakingRoutes } from '@onekeyhq/shared/src/routes'; +import { ETabEarnRoutes, ETabRoutes } from '@onekeyhq/shared/src/routes'; import { RawActions } from './RawActions'; @@ -92,7 +91,6 @@ export function WalletActionEarn(props: { isSoftwareWalletOnlyUser, }); - // Convert protocol list to the format expected by showProtocolListDialog const protocols = protocolList.map((protocol) => ({ provider: protocol.provider.name, networkId: protocol.network.networkId, @@ -101,8 +99,8 @@ export function WalletActionEarn(props: { if (protocols.length === 1) { const protocol = protocolList[0]; - navigation.pushModal(EModalRoutes.StakingModal, { - screen: EModalStakingRoutes.ProtocolDetailsV2, + navigation.switchTab(ETabRoutes.Earn, { + screen: ETabEarnRoutes.EarnProtocolDetails, params: { networkId, accountId, @@ -115,17 +113,12 @@ export function WalletActionEarn(props: { return; } - // Use dialog for multiple protocols - showProtocolListDialog({ - symbol, - accountId, - indexedAccountId, - filterNetworkId: networkId, - onProtocolSelect: async (params) => { - navigation.pushModal(EModalRoutes.StakingModal, { - screen: EModalStakingRoutes.ProtocolDetailsV2, - params, - }); + // Navigate to protocols list page for multiple protocols + navigation.switchTab(ETabRoutes.Earn, { + screen: ETabEarnRoutes.EarnProtocols, + params: { + symbol, + filterNetworkId: networkId, }, }); }, [ diff --git a/packages/kit/src/views/Market/components/Chart/ChartViewAdapter.tsx b/packages/kit/src/views/Market/components/Chart/ChartViewAdapter.tsx index 2b7b34ef517f..210e35c3113e 100644 --- a/packages/kit/src/views/Market/components/Chart/ChartViewAdapter.tsx +++ b/packages/kit/src/views/Market/components/Chart/ChartViewAdapter.tsx @@ -3,6 +3,8 @@ import { useEffect, useRef } from 'react'; import { createChart } from 'lightweight-charts'; +import { useThemeValue } from '@onekeyhq/components'; + import { createChartDom, updateChartDom } from './chartUtils'; import type { IChartViewAdapterProps } from './chartUtils'; @@ -16,6 +18,7 @@ const ChartViewAdapter: FC = ({ height, }) => { const chartContainerRef = useRef(null); + const textSubduedColor = useThemeValue('textSubdued'); useEffect(() => { if (!chartContainerRef.current) { @@ -26,6 +29,7 @@ const ChartViewAdapter: FC = ({ chartContainerRef.current, onHover, height, + textSubduedColor, ); return () => { @@ -33,7 +37,7 @@ const ChartViewAdapter: FC = ({ chart.remove(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [textSubduedColor]); useEffect(() => { updateChartDom({ diff --git a/packages/kit/src/views/Market/components/Chart/chartUtils.ts b/packages/kit/src/views/Market/components/Chart/chartUtils.ts index 25dea132c152..8a52f68d54e7 100644 --- a/packages/kit/src/views/Market/components/Chart/chartUtils.ts +++ b/packages/kit/src/views/Market/components/Chart/chartUtils.ts @@ -51,6 +51,7 @@ export function createChartDom( domNode: HTMLElement, onHover: IOnHoverFunction, height: number, + textColor?: string, ) { const chart = createChartFunc(domNode, { height, @@ -58,20 +59,36 @@ export function createChartDom( background: { color: 'transparent', }, + textColor, }, crosshair: { - vertLine: { visible: false }, - horzLine: { visible: false }, + vertLine: { + visible: false, + labelVisible: false, + }, + horzLine: { + visible: false, + labelVisible: false, + }, }, grid: { vertLines: { visible: false }, horzLines: { visible: false }, }, timeScale: { - visible: false, + visible: true, fixLeftEdge: true, fixRightEdge: true, lockVisibleTimeRangeOnResize: true, + borderVisible: false, + timeVisible: false, + secondsVisible: false, + tickMarkFormatter: (time: UTCTimestamp | BusinessDay) => { + const date = new Date((time as number) * 1000); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${month}/${day}`; + }, }, rightPriceScale: { visible: false, @@ -120,8 +137,12 @@ export function updateChartDom({ topColor, bottomColor, lineWidth: 2, - crosshairMarkerBorderColor: '#fff', - crosshairMarkerRadius: 5, + priceLineVisible: false, + lastValueVisible: false, + crosshairMarkerVisible: true, + crosshairMarkerRadius: 4, + crosshairMarkerBorderColor: lineColor, + crosshairMarkerBackgroundColor: '#FFFFFF', }); newSeries.setData(formattedData); chart._onekey_series = newSeries; diff --git a/packages/kit/src/views/Market/components/tradeHook.tsx b/packages/kit/src/views/Market/components/tradeHook.tsx index a233e6ea4ed1..9d0bde9006ca 100644 --- a/packages/kit/src/views/Market/components/tradeHook.tsx +++ b/packages/kit/src/views/Market/components/tradeHook.tsx @@ -7,7 +7,8 @@ import { Dialog, SizableText } from '@onekeyhq/components'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; import { - EModalStakingRoutes, + ETabEarnRoutes, + ETabRoutes, type IModalSwapParamList, } from '@onekeyhq/shared/src/routes'; import { EModalRoutes } from '@onekeyhq/shared/src/routes/modal'; @@ -32,7 +33,6 @@ import { import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; -import { showProtocolListDialog } from '../../Earn/components/showProtocolListDialog'; export const useMarketTradeNetwork = (token: IMarketTokenDetail | null) => { const { detailPlatforms, platforms = {} } = token || {}; @@ -255,16 +255,12 @@ export const useMarketTradeActions = (token: IMarketTokenDetail | null) => { return; } - // Use dialog for multiple protocols, pass minimal required fields - showProtocolListDialog({ - symbol: normalizedSymbol, - accountId: networkAccount.id, - indexedAccountId: networkAccount.indexedAccountId, - onProtocolSelect: async (params) => { - navigation.pushModal(EModalRoutes.StakingModal, { - screen: EModalStakingRoutes.ProtocolDetailsV2, - params, - }); + // Navigate to protocols list page + navigation.switchTab(ETabRoutes.Earn, { + screen: ETabEarnRoutes.EarnProtocols, + params: { + symbol: normalizedSymbol, + filterNetworkId: networkId, }, }); }, [createAccountIfNotExists, navigation, networkId, symbol]); diff --git a/packages/kit/src/views/Staking/components/ProtocolDetails/EarnActionIcon.tsx b/packages/kit/src/views/Staking/components/ProtocolDetails/EarnActionIcon.tsx index 3124db5612dc..66eab374c5d6 100644 --- a/packages/kit/src/views/Staking/components/ProtocolDetails/EarnActionIcon.tsx +++ b/packages/kit/src/views/Staking/components/ProtocolDetails/EarnActionIcon.tsx @@ -41,6 +41,16 @@ import { useHandleClaim } from '../../pages/ProtocolDetails/useHandleClaim'; import { EarnIcon } from './EarnIcon'; import { EarnText } from './EarnText'; +type IActionTrigger = ({ + onPress, + loading, + disabled, +}: { + onPress: () => void; + loading?: boolean; + disabled?: boolean; +}) => React.ReactNode; + // Hook to handle claim action press function useHandleClaimAction({ protocolInfo, @@ -76,6 +86,8 @@ function useHandleClaimAction({ providerName: tokenInfo?.provider, }) ); + const receiveToken = earnUtils.convertEarnTokenToIToken(token); + await handleClaim({ claimType: actionIcon.type, symbol: protocolInfo?.symbol || '', @@ -95,12 +107,12 @@ function useHandleClaimAction({ providerName: tokenInfo?.provider || '', }), protocolLogoURI: protocolInfo?.providerDetail.logoURI, - receive: { - token: token as IEarnToken, - amount: claimAmount, - }, + receive: receiveToken + ? { token: receiveToken, amount: claimAmount } + : undefined, tags: protocolInfo?.stakeTag ? [protocolInfo.stakeTag] : [], }, + portfolioSymbol: token?.symbol, }); setLoading(false); }, @@ -240,10 +252,12 @@ function BasicPortfolioActionIcon({ actionIcon, protocolInfo, tokenInfo, + trigger, }: { protocolInfo?: IProtocolInfo; tokenInfo?: IEarnTokenInfo; actionIcon: IEarnPortfolioActionIcon; + trigger?: IActionTrigger; }) { const appNavigation = useAppNavigation(); @@ -261,6 +275,15 @@ function BasicPortfolioActionIcon({ protocolInfo?.symbol, tokenInfo?.networkId, ]); + + if (trigger) { + return trigger({ + onPress: onPortfolioDetails, + loading: false, + disabled: actionIcon.disabled, + }); + } + return ( + + ); + } + + return null; +}; diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/components/ManagePositionContent.tsx b/packages/kit/src/views/Staking/pages/ManagePosition/components/ManagePositionContent.tsx new file mode 100644 index 000000000000..798b651cb60d --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/components/ManagePositionContent.tsx @@ -0,0 +1,548 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useNavigation } from '@react-navigation/native'; +import { useIntl } from 'react-intl'; +import { useSharedValue } from 'react-native-reanimated'; + +import { + Button, + SizableText, + Skeleton, + Stack, + Tabs, + XStack, + YStack, +} from '@onekeyhq/components'; +import { useMedia } from '@onekeyhq/components/src/hooks'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accountSelector'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { + EModalReceiveRoutes, + EModalRoutes, + EModalStakingRoutes, +} from '@onekeyhq/shared/src/routes'; +import type { ISupportedSymbol } from '@onekeyhq/shared/types/earn'; +import { EStakingActionType } from '@onekeyhq/shared/types/staking'; + +import { EarnAlert } from '../../../components/ProtocolDetails/EarnAlert'; +import { EarnText } from '../../../components/ProtocolDetails/EarnText'; +import { NoAddressWarning } from '../../../components/ProtocolDetails/NoAddressWarning'; +import { useHandleSwap } from '../../../hooks/useHandleSwap'; +import { useManagePage } from '../hooks/useManagePage'; + +import { HeaderRight } from './HeaderRight'; +import { buildCustomContent } from './protocolConfigs'; +import { StakeSection } from './StakeSection'; +import { WithdrawSection } from './WithdrawSection'; + +export interface IManagePositionContentProps { + // Essential params + networkId: string; + symbol: string; + provider: string; + vault?: string; + accountId: string; + indexedAccountId?: string; + + // Optional configurations + defaultTab?: 'deposit' | 'withdraw'; + onTabChange?: (tab: 'deposit' | 'withdraw') => void; + + // Optional callbacks + onCreateAddress?: () => Promise; + onStakeWithdrawSuccess?: () => void; +} + +export function ManagePositionContent({ + networkId, + symbol, + provider, + vault, + accountId, + indexedAccountId, + defaultTab, + onTabChange, + onCreateAddress, + onStakeWithdrawSuccess, +}: IManagePositionContentProps) { + const intl = useIntl(); + const appNavigation = useAppNavigation(); + const navigation = useNavigation(); + const { gtMd } = useMedia(); + const { activeAccount } = useActiveAccount({ num: 0 }); + const { handleSwap } = useHandleSwap(); + + const { account, indexedAccount: activeIndexedAccount } = activeAccount; + + // Use managePage hook to fetch all data + const { + isLoading, + tokenInfo, + earnAccount, + protocolInfo, + managePageData, + depositDisabled, + withdrawDisabled, + alertsStake, + alertsWithdraw, + subscriptionValue, + detailActions, + refreshAccount: refreshManageAccount, + run: refreshManageData, + } = useManagePage({ + accountId, + networkId, + indexedAccountId, + symbol: symbol as ISupportedSymbol, + provider, + vault, + }); + + // Handle create address + const handleCreateAddress = useCallback(async () => { + if (onCreateAddress) { + await onCreateAddress(); + } + await refreshManageAccount(); + await refreshManageData(); + }, [onCreateAddress, refreshManageAccount, refreshManageData]); + + const hasNoAccount = !accountId && !indexedAccountId; + const hasNoAddress = !earnAccount?.accountAddress; + + // USDe handlers + const handleReceive = useCallback(() => { + if (!subscriptionValue?.token?.info || !earnAccount) return; + appNavigation.pushModal(EModalRoutes.ReceiveModal, { + screen: EModalReceiveRoutes.ReceiveToken, + params: { + networkId, + accountId: earnAccount.accountId, + walletId: earnAccount.walletId, + token: subscriptionValue.token.info, + }, + }); + }, [appNavigation, networkId, earnAccount, subscriptionValue?.token?.info]); + + const handleTrade = useCallback(async () => { + if (!subscriptionValue?.token?.info) return; + await handleSwap({ + token: subscriptionValue.token.info, + networkId, + }); + }, [handleSwap, networkId, subscriptionValue?.token?.info]); + + // Build custom content + const customContent = useMemo( + () => + buildCustomContent( + { symbol, provider, vault }, + { + subscriptionValue, + detailActions, + handlers: { + onReceive: handleReceive, + onTrade: handleTrade, + }, + }, + ), + [ + symbol, + provider, + vault, + subscriptionValue, + detailActions, + handleReceive, + handleTrade, + ], + ); + + const renderNoAddressWarning = useCallback( + () => + hasNoAccount || hasNoAddress ? ( + + + + ) : null, + [ + hasNoAccount, + hasNoAddress, + accountId, + networkId, + indexedAccountId, + handleCreateAddress, + ], + ); + + const historyAction = useMemo( + () => managePageData?.history, + [managePageData?.history], + ); + + // Determine if we're in a modal context + const isInModalContext = useMemo(() => { + try { + const state = navigation.getState?.(); + const currentRoute = state?.routes?.[state.index]; + return currentRoute?.name?.includes('Modal') ?? false; + } catch { + return false; + } + }, [navigation]); + + const onHistory = useMemo(() => { + if (historyAction?.disabled || !earnAccount?.accountId) return undefined; + return (params?: { filterType?: string }) => { + const { filterType } = params || {}; + const historyParams = { + accountId: earnAccount?.accountId, + networkId, + symbol, + provider, + stakeTag: protocolInfo?.stakeTag || '', + protocolVault: vault, + filterType, + }; + + if (isInModalContext) { + // We're already in a modal, use push to navigate within the modal stack + appNavigation.push(EModalStakingRoutes.HistoryList, historyParams); + } else { + // We're in a regular page (like EarnProtocolDetails), use pushModal + appNavigation.pushModal(EModalRoutes.StakingModal, { + screen: EModalStakingRoutes.HistoryList, + params: historyParams, + }); + } + }; + }, [ + historyAction?.disabled, + appNavigation, + earnAccount?.accountId, + networkId, + protocolInfo?.stakeTag, + provider, + symbol, + vault, + isInModalContext, + ]); + + // Initialize selectedTabIndex based on defaultTab + const [selectedTabIndex, setSelectedTabIndex] = useState(() => { + if (defaultTab === 'withdraw') return 1; + return 0; + }); + + // Update selectedTabIndex when defaultTab changes from route + useEffect(() => { + if (defaultTab === 'withdraw') { + setSelectedTabIndex(1); + } else if (defaultTab === 'deposit') { + setSelectedTabIndex(0); + } + }, [defaultTab]); + + const tabData = useMemo( + () => [ + { + title: intl.formatMessage({ id: ETranslations.earn_deposit }), + type: EStakingActionType.Deposit, + }, + { + title: intl.formatMessage({ id: ETranslations.global_withdraw }), + type: EStakingActionType.Withdraw, + }, + ], + [intl], + ); + + const TabNames = useMemo(() => { + return tabData.map((item) => item.title); + }, [tabData]); + + // Initialize focusedTab based on defaultTab + const initialTabName = useMemo(() => { + if (defaultTab === 'withdraw') return TabNames[1]; + return TabNames[0]; + }, [defaultTab, TabNames]); + + const focusedTab = useSharedValue(initialTabName); + + const handleStakeWithdrawSuccess = useCallback(() => { + if (isInModalContext) { + appNavigation.pop(); + } + // If not in modal, don't navigate (stay on current page) + // Call parent refresh callback to update data + onStakeWithdrawSuccess?.(); + }, [isInModalContext, appNavigation, onStakeWithdrawSuccess]); + + const handleTabChange = useCallback( + (name: string) => { + const index = tabData.findIndex((item) => item.title === name); + if (index !== -1) { + // Check if clicking Withdraw tab and it's a withdrawOrder type + if ( + index === 1 && + protocolInfo?.withdrawAction?.type === + EStakingActionType.WithdrawOrder + ) { + // Directly open WithdrawOptions modal instead of switching tab + const withdrawParams = { + accountId: earnAccount?.accountId || '', + networkId, + protocolInfo, + tokenInfo, + symbol, + provider, + }; + + // Check navigation context to use appropriate navigation method + if (isInModalContext) { + appNavigation.push( + EModalStakingRoutes.WithdrawOptions, + withdrawParams, + ); + } else { + appNavigation.pushModal(EModalRoutes.StakingModal, { + screen: EModalStakingRoutes.WithdrawOptions, + params: withdrawParams, + }); + } + return; + } + + focusedTab.value = name; + setSelectedTabIndex(index); + + // Notify parent component if callback provided + const newTab = index === 0 ? 'deposit' : 'withdraw'; + onTabChange?.(newTab); + + // Update route params if navigation is available + if (navigation.setParams) { + navigation.setParams({ + tab: newTab, + } as any); + } + } + }, + [ + earnAccount?.accountId, + focusedTab, + tabData, + navigation, + protocolInfo, + appNavigation, + networkId, + tokenInfo, + symbol, + provider, + onTabChange, + isInModalContext, + ], + ); + + // Custom content rendering for special protocols + const customActions = useMemo(() => { + if (!customContent?.actions) return []; + return customContent.actions; + }, [customContent?.actions]); + + const renderCustomActionButtons = useCallback(() => { + if (!gtMd || !customActions.length || !customContent?.handlers) { + return null; + } + + return ( + + {customActions.map((action) => { + const handlerKey = `on${action.type + .charAt(0) + .toUpperCase()}${action.type.slice(1)}`; + const handler = customContent.handlers?.[handlerKey]; + + if (!handler || action.disabled) return null; + + const isReceive = action.type === EStakingActionType.Receive; + const isTrade = action.type === EStakingActionType.Trade; + + // Determine translation key + let translationKey = ETranslations.global_continue; + if (isReceive) { + translationKey = ETranslations.global_receive; + } else if (isTrade) { + translationKey = ETranslations.global_trade; + } + + return ( + + ); + })} + + ); + }, [gtMd, customActions, customContent?.handlers, intl]); + + // Show loading skeleton + if (isLoading && !hasNoAccount) { + return ( + + {/* Tabs skeleton */} + + + + + + {/* Input section skeleton */} + + + + + + + + + + + + + + + + {/* Info cards skeleton */} + + + + + + + + + + + + + + + + {/* Button skeleton */} + + + + ); + } + + if (hasNoAccount || hasNoAddress) { + // Show NoAddressWarning instead of content + return <>{renderNoAddressWarning()}; + } + + // Custom content rendering (e.g., USDe or other special protocols) + if (customContent?.data) { + const data = customContent.data; + return ( + <> + + + + + + + + + {renderCustomActionButtons()} + + + + + + + ); + } + + // Normal deposit/withdraw rendering + // If no tokenInfo, return null (loading state should have been handled above) + if (!tokenInfo) { + return null; + } + + return ( + <> + + ( + handleTabChange(name)} + > + + {name} + + + )} + /> + + + {selectedTabIndex === 0 ? ( + <> + } + /> + + ) : null} + {selectedTabIndex === 1 ? ( + <> + } + /> + {renderNoAddressWarning()} + + ) : null} + + ); +} diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/components/StakeSection.tsx b/packages/kit/src/views/Staking/pages/ManagePosition/components/StakeSection.tsx new file mode 100644 index 000000000000..f85c9253b819 --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/components/StakeSection.tsx @@ -0,0 +1,279 @@ +import type { ReactElement } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import { useEarnActions } from '@onekeyhq/kit/src/states/jotai/contexts/earn/actions'; +import { MorphoBundlerContract } from '@onekeyhq/shared/src/consts/addresses'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; +import earnUtils from '@onekeyhq/shared/src/utils/earnUtils'; +import networkUtils from '@onekeyhq/shared/src/utils/networkUtils'; +import { EEarnProviderEnum } from '@onekeyhq/shared/types/earn'; +import type { IFeeUTXO } from '@onekeyhq/shared/types/fee'; +import { EApproveType, EEarnLabels } from '@onekeyhq/shared/types/staking'; +import type { + IApproveConfirmFnParams, + IEarnTokenInfo, + IProtocolInfo, +} from '@onekeyhq/shared/types/staking'; +import type { IToken } from '@onekeyhq/shared/types/token'; + +import { UniversalStake } from '../../../components/UniversalStake'; +import { useUniversalStake } from '../../../hooks/useUniversalHooks'; + +export const StakeSection = ({ + accountId, + networkId, + tokenInfo, + protocolInfo, + isDisabled, + onSuccess, + beforeFooter, +}: { + accountId: string; + networkId: string; + tokenInfo?: IEarnTokenInfo; + protocolInfo?: IProtocolInfo; + isDisabled?: boolean; + onSuccess?: () => void; + beforeFooter?: ReactElement | null; +}) => { + // Early return if no tokenInfo or protocolInfo + // This happens when there's no account or no address + const hasRequiredData = tokenInfo && protocolInfo; + + const { result: estimateFeeUTXO } = usePromiseResult(async () => { + if (!hasRequiredData || !networkUtils.isBTCNetwork(networkId)) { + return; + } + const account = await backgroundApiProxy.serviceAccount.getAccount({ + accountId, + networkId, + }); + const accountAddress = account.address; + const result = await backgroundApiProxy.serviceGas.estimateFee({ + accountId, + networkId, + accountAddress, + }); + return result.feeUTXO?.filter( + (o): o is Required> => o.feeRate !== undefined, + ); + }, [accountId, networkId, hasRequiredData]); + + const [btcFeeRate, setBtcFeeRate] = useState(); + const btcFeeRateInit = useRef(false); + const { removePermitCache } = useEarnActions().current; + + const onFeeRateChange = useMemo(() => { + if ( + protocolInfo?.provider.toLowerCase() === + EEarnProviderEnum.Babylon.toLowerCase() + ) { + return (value: string) => setBtcFeeRate(value); + } + }, [protocolInfo?.provider]); + + useEffect(() => { + if ( + estimateFeeUTXO && + estimateFeeUTXO.length === 3 && + !btcFeeRateInit.current + ) { + const [, normalFee] = estimateFeeUTXO; + setBtcFeeRate(normalFee.feeRate); + btcFeeRateInit.current = true; + } + }, [estimateFeeUTXO]); + + const { result, isLoading } = usePromiseResult( + async () => { + if (!hasRequiredData || !protocolInfo?.approve?.approveTarget) { + return undefined; + } + if (protocolInfo?.approve?.approveTarget) { + // For vault-based providers, check allowance against vault address + const isVaultBased = earnUtils.isVaultBasedProvider({ + providerName: protocolInfo.provider, + }); + + // Determine the correct spender address for allowance check + let spenderAddress = protocolInfo.approve.approveTarget; + if (protocolInfo.approve?.approveType === EApproveType.Permit) { + spenderAddress = MorphoBundlerContract; + } else if (isVaultBased) { + spenderAddress = protocolInfo.vault ?? ''; + } + + const { allowanceParsed } = + await backgroundApiProxy.serviceStaking.fetchTokenAllowance({ + accountId, + networkId, + spenderAddress, + tokenAddress: tokenInfo?.token.address || '', + }); + + return { allowanceParsed }; + } + + return undefined; + }, + [ + hasRequiredData, + accountId, + networkId, + protocolInfo?.approve?.approveTarget, + protocolInfo?.approve?.approveType, + protocolInfo?.provider, + protocolInfo?.vault, + tokenInfo?.token.address, + ], + { + watchLoading: true, + }, + ); + + const handleStake = useUniversalStake({ accountId, networkId }); + const appNavigation = useAppNavigation(); + + const onConfirm = useCallback( + async ({ + amount, + approveType, + permitSignature, + }: IApproveConfirmFnParams) => { + if (!hasRequiredData) return; + + const providerName = protocolInfo?.provider ?? ''; + const token = tokenInfo?.token as IToken; + const symbol = tokenInfo?.token.symbol || ''; + + await handleStake({ + amount, + approveType, + permitSignature, + symbol, + provider: providerName, + stakingInfo: { + label: EEarnLabels.Stake, + protocol: earnUtils.getEarnProviderName({ + providerName, + }), + protocolLogoURI: protocolInfo?.providerDetail.logoURI, + send: { token, amount }, + tags: [protocolInfo?.stakeTag || ''], + }, + // TODO: remove term after babylon remove term + term: undefined, + feeRate: Number(btcFeeRate) > 0 ? Number(btcFeeRate) : undefined, + protocolVault: earnUtils.isVaultBasedProvider({ + providerName, + }) + ? protocolInfo?.vault + : undefined, + onSuccess: async (txs) => { + onSuccess?.(); + defaultLogger.staking.page.staking({ + token, + stakingProtocol: providerName, + }); + const tx = txs[0]; + if (approveType === EApproveType.Permit && permitSignature) { + removePermitCache({ + accountId, + networkId, + tokenAddress: tokenInfo?.token.address || '', + amount, + }); + } + if ( + tx && + providerName.toLowerCase() === + EEarnProviderEnum.Babylon.toLowerCase() + ) { + await backgroundApiProxy.serviceStaking.addBabylonTrackingItem({ + txId: tx.decodedTx.txid, + action: 'stake', + createAt: Date.now(), + accountId, + networkId, + amount, + // TODO: remove term after babylon remove term + minStakeTerm: undefined, + }); + } + }, + }); + }, + [ + hasRequiredData, + tokenInfo?.token, + handleStake, + protocolInfo?.providerDetail.logoURI, + protocolInfo?.vault, + btcFeeRate, + onSuccess, + removePermitCache, + accountId, + networkId, + protocolInfo?.provider, + protocolInfo?.stakeTag, + ], + ); + + if (isLoading) { + // FIXME: ... + return null; + } + + // If no required data, render placeholder to maintain layout + if (!hasRequiredData) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/components/WithdrawSection.tsx b/packages/kit/src/views/Staking/pages/ManagePosition/components/WithdrawSection.tsx new file mode 100644 index 000000000000..2d0cedd80747 --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/components/WithdrawSection.tsx @@ -0,0 +1,141 @@ +import type { ReactElement } from 'react'; +import { useCallback, useMemo } from 'react'; + +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; +import earnUtils from '@onekeyhq/shared/src/utils/earnUtils'; +import { + EEarnLabels, + type IEarnTokenInfo, + type IProtocolInfo, +} from '@onekeyhq/shared/types/staking'; +import type { IToken } from '@onekeyhq/shared/types/token'; + +import { UniversalWithdraw } from '../../../components/UniversalWithdraw'; +import { useUniversalWithdraw } from '../../../hooks/useUniversalHooks'; + +export const WithdrawSection = ({ + accountId, + networkId, + tokenInfo, + protocolInfo, + isDisabled, + onSuccess, + beforeFooter, +}: { + accountId: string; + networkId: string; + tokenInfo?: IEarnTokenInfo; + protocolInfo?: IProtocolInfo; + isDisabled?: boolean; + onSuccess?: () => void; + beforeFooter?: ReactElement | null; +}) => { + // Early return if no tokenInfo or protocolInfo + // This happens when there's no account or no address + const hasRequiredData = tokenInfo && protocolInfo; + + const providerName = useMemo( + () => protocolInfo?.provider ?? '', + [protocolInfo?.provider], + ); + const token = useMemo(() => tokenInfo?.token as IToken, [tokenInfo]); + const symbol = useMemo(() => token?.symbol || '', [token]); + const vault = useMemo(() => protocolInfo?.vault || '', [protocolInfo?.vault]); + const handleWithdraw = useUniversalWithdraw({ accountId, networkId }); + const appNavigation = useAppNavigation(); + + const onConfirm = useCallback( + async ({ + amount, + withdrawAll, + }: { + amount: string; + withdrawAll: boolean; + }) => { + if (!hasRequiredData) return; + + await handleWithdraw({ + amount, + // identity, + protocolVault: earnUtils.isVaultBasedProvider({ + providerName, + }) + ? vault + : undefined, + symbol, + provider: providerName, + stakingInfo: { + label: EEarnLabels.Withdraw, + protocol: earnUtils.getEarnProviderName({ + providerName, + }), + protocolLogoURI: protocolInfo?.providerDetail.logoURI, + tags: [protocolInfo?.stakeTag || ''], + }, + withdrawAll, + onSuccess: () => { + onSuccess?.(); + defaultLogger.staking.page.unstaking({ + token, + stakingProtocol: providerName, + }); + }, + }); + }, + [ + hasRequiredData, + handleWithdraw, + // identity, + providerName, + vault, + protocolInfo?.providerDetail.logoURI, + onSuccess, + token, + protocolInfo?.stakeTag, + symbol, + ], + ); + + // If no required data, render placeholder to maintain layout + if (!hasRequiredData) { + return ( + {}} + protocolVault="" + isDisabled + /> + ); + } + + return ( + 0 + ? String(protocolInfo?.minUnstakeAmount) + : undefined + } + protocolVault={protocolInfo?.vault ?? ''} + isDisabled={isDisabled} + beforeFooter={beforeFooter} + /> + ); +}; diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/components/protocolConfigs.ts b/packages/kit/src/views/Staking/pages/ManagePosition/components/protocolConfigs.ts new file mode 100644 index 000000000000..0cae43dc8b60 --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/components/protocolConfigs.ts @@ -0,0 +1,122 @@ +/** + * Protocol-specific configurations for ManagePosition page + * + * This file defines custom rendering configurations for special protocols + * that don't follow the standard deposit/withdraw pattern. + * + * Example: USDe uses a subscription model with Receive/Trade actions + * instead of the standard Deposit/Withdraw flow. + */ + +import type { IStakeEarnDetail } from '@onekeyhq/shared/types/staking'; + +export interface IProtocolCustomConfig { + // Identifies protocols that need custom rendering + shouldUseCustomContent: (params: { + symbol: string; + provider: string; + vault?: string; + }) => boolean; + + // Build custom content configuration + buildCustomContent?: (data: { + subscriptionValue?: IStakeEarnDetail['subscriptionValue']; + detailActions?: IStakeEarnDetail['actions']; + handlers: { + onReceive?: () => void; + onTrade?: () => void; + [key: string]: (() => void) | undefined; + }; + }) => + | { + data?: IStakeEarnDetail['subscriptionValue']; + actions?: IStakeEarnDetail['actions']; + handlers?: { + onReceive?: () => void; + onTrade?: () => void; + [key: string]: (() => void) | undefined; + }; + } + | undefined; +} + +/** + * USDe Protocol Configuration + * + * USDe uses a subscription-based model where users: + * - View their subscription value (holdings) + * - Can receive more USDe + * - Can trade USDe + */ +export const usdeProtocolConfig: IProtocolCustomConfig = { + shouldUseCustomContent: ({ symbol }) => symbol === 'USDe', + + buildCustomContent: ({ subscriptionValue, detailActions, handlers }) => { + if (!subscriptionValue) return undefined; + + return { + data: subscriptionValue, + actions: detailActions, + handlers, + }; + }, +}; + +/** + * Registry of all protocol-specific configurations + * + * To add a new custom protocol: + * 1. Create a configuration object following IProtocolCustomConfig + * 2. Add it to this array + * 3. The ManagePositionContent will automatically use it + */ +export const protocolConfigs: IProtocolCustomConfig[] = [ + usdeProtocolConfig, + // Add more custom protocol configurations here + // Example: + // { + // shouldUseCustomContent: ({ symbol, provider }) => + // symbol === 'CUSTOM_TOKEN' && provider === 'CustomProvider', + // buildCustomContent: ({ subscriptionValue, detailActions, handlers }) => ({ + // data: subscriptionValue, + // actions: detailActions, + // handlers, + // }), + // }, +]; + +/** + * Helper function to check if a protocol needs custom rendering + */ +export function shouldUseCustomContent(params: { + symbol: string; + provider: string; + vault?: string; +}): boolean { + return protocolConfigs.some((config) => + config.shouldUseCustomContent(params), + ); +} + +/** + * Helper function to build custom content configuration + */ +export function buildCustomContent( + params: { + symbol: string; + provider: string; + vault?: string; + }, + data: { + subscriptionValue?: IStakeEarnDetail['subscriptionValue']; + detailActions?: IStakeEarnDetail['actions']; + handlers: { + onReceive?: () => void; + onTrade?: () => void; + [key: string]: (() => void) | undefined; + }; + }, +) { + const config = protocolConfigs.find((c) => c.shouldUseCustomContent(params)); + return config?.buildCustomContent?.(data); +} diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useEarnAccount.ts b/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useEarnAccount.ts new file mode 100644 index 000000000000..94f8b540876f --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useEarnAccount.ts @@ -0,0 +1,25 @@ +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; + +export const useEarnAccount = ({ + accountId, + networkId, + indexedAccountId, +}: { + accountId: string; + indexedAccountId: string | undefined; + networkId: string; +}) => { + const { result: earnAccount, run: refreshAccount } = usePromiseResult( + async () => + backgroundApiProxy.serviceStaking.getEarnAccount({ + accountId, + networkId, + indexedAccountId, + btcOnlyTaproot: true, + }), + [accountId, indexedAccountId, networkId], + ); + + return { earnAccount, refreshAccount }; +}; diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useManagePage.ts b/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useManagePage.ts new file mode 100644 index 000000000000..a8a7c37fbcb4 --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useManagePage.ts @@ -0,0 +1,248 @@ +import { useMemo } from 'react'; + +import BigNumber from 'bignumber.js'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import networkUtils from '@onekeyhq/shared/src/utils/networkUtils'; +import type { ISupportedSymbol } from '@onekeyhq/shared/types/earn'; +import type { + EApproveType, + IEarnTokenInfo, + IEarnWithdrawActionIcon, + IProtocolInfo, + IStakeProtocolListItem, +} from '@onekeyhq/shared/types/staking'; + +import { buildLocalTxStatusSyncId } from '../../../utils/utils'; + +import { useEarnAccount } from './useEarnAccount'; + +export const useManagePage = ({ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, +}: { + accountId: string; + indexedAccountId: string | undefined; + networkId: string; + symbol: ISupportedSymbol; + provider: string; + vault: string | undefined; +}) => { + const { earnAccount, refreshAccount } = useEarnAccount({ + accountId, + networkId, + indexedAccountId, + }); + + const { + result: managePageData, + isLoading: isLoadingManagePage, + run, + } = usePromiseResult( + async () => { + if (!earnAccount?.accountAddress) { + return undefined; + } + + const response = await backgroundApiProxy.serviceStaking.getManagePage({ + networkId, + symbol, + provider, + vault, + accountAddress: earnAccount.accountAddress, + publicKey: networkUtils.isBTCNetwork(networkId) + ? earnAccount.account.pub + : undefined, + }); + return response; + }, + [earnAccount, networkId, symbol, provider, vault], + { watchLoading: true }, + ); + + // Fetch protocol list to get provider details + const { result: protocolList, isLoading: isLoadingProtocolList } = + usePromiseResult( + async () => { + if (!accountId) { + return undefined; + } + + const protocols = + await backgroundApiProxy.serviceStaking.getProtocolList({ + symbol, + accountId, + indexedAccountId, + filterNetworkId: networkId, + }); + return protocols; + }, + [accountId, indexedAccountId, symbol, networkId], + { watchLoading: true }, + ); + + const isLoading = isLoadingManagePage || isLoadingProtocolList; + + const tokenInfo: IEarnTokenInfo | undefined = useMemo(() => { + if (!managePageData?.deposit?.data?.token) { + return undefined; + } + + const balanceBN = new BigNumber(managePageData.deposit.data.balance || '0'); + const balanceParsed = balanceBN.isNaN() ? '0' : balanceBN.toFixed(); + + return { + balanceParsed, + token: managePageData.deposit.data.token.info, + price: managePageData.deposit.data.token.price, + networkId, + provider, + vault, + accountId, + }; + }, [ + managePageData?.deposit?.data?.token, + managePageData?.deposit?.data?.balance, + networkId, + provider, + vault, + accountId, + ]); + + const protocolInfo: IProtocolInfo | undefined = useMemo(() => { + if (!managePageData) { + return undefined; + } + + // Find the matching protocol from protocol list + const matchingProtocol = protocolList?.find( + (item: IStakeProtocolListItem) => + item.provider.name.toLowerCase() === provider.toLowerCase() && + item.network.networkId === networkId && + (!vault || item.provider.vault === vault), + ); + + // Get withdraw action from managePageData + const withdrawAction = managePageData.withdraw as + | IEarnWithdrawActionIcon + | undefined; + + return { + symbol, + provider, + vault, + networkId, + earnAccount, + activeBalance: managePageData.withdraw?.data?.balance, + stakeTag: buildLocalTxStatusSyncId({ + providerName: provider, + tokenSymbol: symbol, + }), + providerDetail: { + name: matchingProtocol?.provider.name || provider, + logoURI: matchingProtocol?.provider.logoURI || '', + }, + // withdraw + withdrawAction, + overflowBalance: managePageData.nums?.overflow, + maxUnstakeAmount: managePageData.nums?.maxUnstakeAmount, + minUnstakeAmount: managePageData.nums?.minUnstakeAmount, + // staking + minTransactionFee: managePageData.nums?.minTransactionFee, + remainingCap: managePageData.nums?.remainingCap, + // claim + claimable: managePageData.nums?.claimable, + // approve + approve: managePageData.approve + ? { + allowance: managePageData.approve.allowance, + approveType: managePageData.approve + .approveType as unknown as EApproveType, + approveTarget: managePageData.approve.approveTarget, + } + : undefined, + } as IProtocolInfo; + }, [ + managePageData, + protocolList, + symbol, + provider, + vault, + networkId, + earnAccount, + ]); + + const depositDisabled = useMemo( + () => managePageData?.deposit?.disabled ?? false, + [managePageData?.deposit?.disabled], + ); + + const withdrawDisabled = useMemo( + () => managePageData?.withdraw?.disabled ?? false, + [managePageData?.withdraw?.disabled], + ); + + const alertsStake = useMemo( + () => managePageData?.alertsStake || [], + [managePageData?.alertsStake], + ); + const alertsWithdraw = useMemo( + () => managePageData?.alertsWithdraw || [], + [managePageData?.alertsWithdraw], + ); + + // Fetch USDe data if symbol is USDe + const { result: usdeDetailInfo, isLoading: isLoadingUsdeDetail } = + usePromiseResult( + async () => { + if (symbol !== 'USDe' || !accountId) { + return undefined; + } + + const response = + await backgroundApiProxy.serviceStaking.getProtocolDetailsV2({ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, + }); + + return response; + }, + [symbol, accountId, networkId, indexedAccountId, provider, vault], + { watchLoading: true }, + ); + + const subscriptionValue = useMemo( + () => usdeDetailInfo?.subscriptionValue, + [usdeDetailInfo?.subscriptionValue], + ); + + const detailActions = useMemo( + () => usdeDetailInfo?.actions, + [usdeDetailInfo?.actions], + ); + + return { + managePageData, + isLoading: isLoading || isLoadingUsdeDetail, + run, + tokenInfo, + earnAccount, + refreshAccount, + protocolInfo, + depositDisabled, + withdrawDisabled, + alertsStake, + alertsWithdraw, + subscriptionValue, + detailActions, + }; +}; diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useProtocolDetails.ts b/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useProtocolDetails.ts new file mode 100644 index 000000000000..81a2d11d5803 --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/hooks/useProtocolDetails.ts @@ -0,0 +1,144 @@ +import { useMemo } from 'react'; + +import BigNumber from 'bignumber.js'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import type { ISupportedSymbol } from '@onekeyhq/shared/types/earn'; +import type { + IEarnTokenInfo, + IEarnWithdrawActionIcon, + IProtocolInfo, +} from '@onekeyhq/shared/types/staking'; + +import { buildLocalTxStatusSyncId } from '../../../utils/utils'; + +import { useEarnAccount } from './useEarnAccount'; + +export const useProtocolDetails = ({ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, +}: { + accountId: string; + indexedAccountId: string | undefined; + networkId: string; + symbol: ISupportedSymbol; + provider: string; + vault: string | undefined; +}) => { + const { earnAccount, refreshAccount } = useEarnAccount({ + accountId, + networkId, + indexedAccountId, + }); + + const { + result: detailInfo, + isLoading, + run, + } = usePromiseResult( + async () => { + const response = + await backgroundApiProxy.serviceStaking.getProtocolDetailsV2({ + accountId, + networkId, + indexedAccountId, + symbol, + provider, + vault, + }); + return response; + }, + [accountId, networkId, indexedAccountId, symbol, provider, vault], + { watchLoading: true }, + ); + + const tokenInfo: IEarnTokenInfo | undefined = useMemo(() => { + if (!detailInfo?.subscriptionValue?.token) { + return undefined; + } + + // Use BigNumber to handle balance and fallback to '0' if invalid or missing + const balanceBN = new BigNumber( + detailInfo.subscriptionValue.balance || '0', + ); + const balanceParsed = balanceBN.isNaN() ? '0' : balanceBN.toFixed(); + + return { + balanceParsed, + token: detailInfo.subscriptionValue.token.info, + price: detailInfo.subscriptionValue.token.price, + networkId, + provider, + vault, + accountId, + }; + }, [ + detailInfo?.subscriptionValue?.token, + detailInfo?.subscriptionValue?.balance, + networkId, + provider, + vault, + accountId, + ]); + + const protocolInfo: IProtocolInfo | undefined = useMemo(() => { + const withdrawAction = detailInfo?.actions?.find( + (i) => i.type === 'withdraw' || i.type === 'withdrawOrder', + ) as IEarnWithdrawActionIcon; + return detailInfo?.protocol + ? { + ...detailInfo.protocol, + apyDetail: detailInfo.apyDetail, + earnAccount, + activeBalance: withdrawAction?.data?.balance, + eventEndTime: detailInfo?.countDownAlert?.endTime, + stakeTag: buildLocalTxStatusSyncId({ + providerName: provider, + tokenSymbol: symbol, + }), + + // withdraw + withdrawAction, + overflowBalance: detailInfo.nums?.overflow, + maxUnstakeAmount: detailInfo.nums?.maxUnstakeAmount, + minUnstakeAmount: detailInfo.nums?.minUnstakeAmount, + + // staking + minTransactionFee: detailInfo.nums?.minTransactionFee, + remainingCap: detailInfo.nums?.remainingCap, + + // claim + claimable: detailInfo.nums?.claimable, + } + : undefined; + }, [ + detailInfo?.actions, + detailInfo?.apyDetail, + detailInfo?.countDownAlert?.endTime, + detailInfo?.nums?.claimable, + detailInfo?.nums?.maxUnstakeAmount, + detailInfo?.nums?.minTransactionFee, + detailInfo?.nums?.minUnstakeAmount, + detailInfo?.nums?.remainingCap, + detailInfo?.nums?.overflow, + detailInfo?.protocol, + earnAccount, + provider, + symbol, + ]); + + return { + detailInfo, + isLoading, + run, + tokenInfo, + earnAccount, + refreshAccount, + protocolInfo, + }; +}; diff --git a/packages/kit/src/views/Staking/pages/ManagePosition/index.tsx b/packages/kit/src/views/Staking/pages/ManagePosition/index.tsx new file mode 100644 index 000000000000..d0fbd393c718 --- /dev/null +++ b/packages/kit/src/views/Staking/pages/ManagePosition/index.tsx @@ -0,0 +1,100 @@ +import { useMemo } from 'react'; + +import { Page } from '@onekeyhq/components'; +import { AccountSelectorProviderMirror } from '@onekeyhq/kit/src/components/AccountSelector'; +import { useAppRoute } from '@onekeyhq/kit/src/hooks/useAppRoute'; +import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accountSelector'; +import { EJotaiContextStoreNames } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; +import { + type EModalStakingRoutes, + type IModalStakingParamList, +} from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import { type ISupportedSymbol } from '@onekeyhq/shared/types/earn'; + +import { DiscoveryBrowserProviderMirror } from '../../../Discovery/components/DiscoveryBrowserProviderMirror'; +import { EarnProviderMirror } from '../../../Earn/EarnProviderMirror'; + +import { ManagePositionContent } from './components/ManagePositionContent'; + +const ManagePositionPage = () => { + const route = useAppRoute< + IModalStakingParamList, + EModalStakingRoutes.ManagePosition + >(); + const { activeAccount } = useActiveAccount({ num: 0 }); + + // parse route params, support two types of routes + const resolvedParams = useMemo<{ + accountId: string; + indexedAccountId: string | undefined; + networkId: string; + symbol: ISupportedSymbol; + provider: string; + vault: string | undefined; + }>(() => { + const routeParams = route.params as any; + + const { + accountId: routeAccountId, + indexedAccountId: routeIndexedAccountId, + networkId, + symbol, + provider, + vault, + } = routeParams; + + return { + accountId: routeAccountId || activeAccount.account?.id || '', + indexedAccountId: + routeIndexedAccountId || activeAccount.indexedAccount?.id, + networkId, + symbol, + provider, + vault, + }; + }, [route.params, activeAccount]); + + const { accountId, indexedAccountId, networkId, symbol, provider, vault } = + resolvedParams; + + // Get tab from route params + const defaultTab = route.params?.tab; + + return ( + + + + + + + ); +}; + +function ManagePositionPageWithProvider() { + return ( + + + + + + + + ); +} + +export default ManagePositionPageWithProvider; diff --git a/packages/kit/src/views/Staking/pages/ProtocolDetails/useHandleClaim.ts b/packages/kit/src/views/Staking/pages/ProtocolDetails/useHandleClaim.ts index d0fb23c4c2b1..28649de85ff8 100644 --- a/packages/kit/src/views/Staking/pages/ProtocolDetails/useHandleClaim.ts +++ b/packages/kit/src/views/Staking/pages/ProtocolDetails/useHandleClaim.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; -import { EModalStakingRoutes } from '@onekeyhq/shared/src/routes'; +import { EModalRoutes, EModalStakingRoutes } from '@onekeyhq/shared/src/routes'; import type { IEarnTokenInfo, IProtocolInfo, @@ -36,6 +36,8 @@ export const useHandleClaim = ({ isReward, stakingInfo, onSuccess, + portfolioSymbol, + portfolioRewardSymbol, }: { claimType: EClaimType; protocolInfo?: IProtocolInfo; @@ -47,6 +49,8 @@ export const useHandleClaim = ({ isMorphoClaim?: boolean; stakingInfo?: IStakingInfo; onSuccess?: () => void; + portfolioSymbol?: string; + portfolioRewardSymbol?: string; }) => { if (!accountId) return; const provider = protocolInfo?.provider || ''; @@ -67,6 +71,9 @@ export const useHandleClaim = ({ provider, stakingInfo, claimTokenAddress, + portfolioSymbol: + portfolioSymbol || tokenInfo?.token?.symbol || undefined, + portfolioRewardSymbol, vault, }); return; @@ -87,13 +94,16 @@ export const useHandleClaim = ({ } if (claimType === EClaimType.ClaimOrder) { - appNavigation.push(EModalStakingRoutes.ClaimOptions, { - accountId, - networkId, - protocolInfo, - tokenInfo, - symbol, - provider, + appNavigation.pushModal(EModalRoutes.StakingModal, { + screen: EModalStakingRoutes.ClaimOptions, + params: { + accountId, + networkId, + protocolInfo, + tokenInfo, + symbol, + provider, + }, }); return; } @@ -110,18 +120,24 @@ export const useHandleClaim = ({ stakingInfo, protocolVault: vault, vault, + portfolioSymbol: + portfolioSymbol || tokenInfo?.token?.symbol || undefined, + portfolioRewardSymbol, }); return; } if (stakingConfig.claimWithTx) { - appNavigation.push(EModalStakingRoutes.ClaimOptions, { - accountId, - networkId, - protocolInfo, - tokenInfo, - symbol, - provider, + appNavigation.pushModal(EModalRoutes.StakingModal, { + screen: EModalStakingRoutes.ClaimOptions, + params: { + accountId, + networkId, + protocolInfo, + tokenInfo, + symbol, + provider, + }, }); return; } @@ -133,6 +149,9 @@ export const useHandleClaim = ({ stakingInfo, protocolVault: vault, vault, + portfolioSymbol: + portfolioSymbol || tokenInfo?.token?.symbol || undefined, + portfolioRewardSymbol, }); }, [accountId, networkId, handleUniversalClaim, appNavigation], diff --git a/packages/kit/src/views/Staking/pages/ProtocolDetailsV2/index.tsx b/packages/kit/src/views/Staking/pages/ProtocolDetailsV2/index.tsx index 6e0f091c27b3..eb7f28151d51 100644 --- a/packages/kit/src/views/Staking/pages/ProtocolDetailsV2/index.tsx +++ b/packages/kit/src/views/Staking/pages/ProtocolDetailsV2/index.tsx @@ -28,6 +28,7 @@ import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accoun import { PeriodSection } from '@onekeyhq/kit/src/views/Staking/components/ProtocolDetails/PeriodSectionV2'; import { ProtectionSection } from '@onekeyhq/kit/src/views/Staking/components/ProtocolDetails/ProtectionSectionV2'; import { + EJotaiContextStoreNames, useDevSettingsPersistAtom, useSettingsPersistAtom, } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; @@ -62,6 +63,7 @@ import type { } from '@onekeyhq/shared/types/staking'; import { showRiskNoticeDialogBeforeDepositOrWithdraw } from '../../../Earn/components/RiskNoticeDialog'; +import { EarnProviderMirror } from '../../../Earn/EarnProviderMirror'; import { EarnNavigation, EarnNetworkUtils } from '../../../Earn/earnUtils'; import { PageFrame, @@ -564,12 +566,61 @@ const ProtocolDetailsPage = () => { provider, vault, }); + return response; }, [accountId, networkId, indexedAccountId, symbol, provider, vault], { watchLoading: true, revalidateOnFocus: true }, ); + const tokenInfo = useMemo(() => { + if (detailInfo?.subscriptionValue?.token) { + const balanceBN = new BigNumber( + detailInfo.subscriptionValue.balance || '0', + ); + const balanceParsed = balanceBN.isNaN() ? '0' : balanceBN.toFixed(); + + return { + balanceParsed, + token: detailInfo.subscriptionValue.token.info, + price: detailInfo.subscriptionValue.token.price, + networkId, + provider, + vault, + accountId: accountId ?? '', + }; + } + return undefined; + }, [detailInfo, networkId, provider, vault, accountId]); + + const protocolInfo = useMemo(() => { + if (!detailInfo?.protocol || !earnAccount) { + return undefined; + } + + const withdrawAction = detailInfo?.actions?.find( + (i) => i.type === 'withdraw', + ) as IEarnWithdrawActionIcon; + + return { + ...detailInfo.protocol, + apyDetail: detailInfo.apyDetail, + earnAccount, + activeBalance: withdrawAction?.data?.balance, + eventEndTime: detailInfo?.countDownAlert?.endTime, + stakeTag: buildLocalTxStatusSyncId({ + providerName: provider, + tokenSymbol: symbol, + }), + overflowBalance: detailInfo.nums?.overflow, + maxUnstakeAmount: detailInfo.nums?.maxUnstakeAmount, + minUnstakeAmount: detailInfo.nums?.minUnstakeAmount, + minTransactionFee: detailInfo.nums?.minTransactionFee, + remainingCap: detailInfo.nums?.remainingCap, + claimable: detailInfo.nums?.claimable, + }; + }, [detailInfo, earnAccount, provider, symbol]); + // Handle unsupported protocol useUnsupportedProtocol({ detailInfo, @@ -582,35 +633,6 @@ const ProtocolDetailsPage = () => { refreshEarnDetailData: run, }); - const tokenInfo: IEarnTokenInfo | undefined = useMemo(() => { - if (!detailInfo?.subscriptionValue?.token) { - return undefined; - } - - // Use BigNumber to handle balance and fallback to '0' if invalid or missing - const balanceBN = new BigNumber( - detailInfo.subscriptionValue.balance || '0', - ); - const balanceParsed = balanceBN.isNaN() ? '0' : balanceBN.toFixed(); - - return { - balanceParsed, - token: detailInfo.subscriptionValue.token.info, - price: detailInfo.subscriptionValue.token.price, - networkId, - provider, - vault, - accountId, - }; - }, [ - detailInfo?.subscriptionValue?.token, - detailInfo?.subscriptionValue?.balance, - networkId, - provider, - vault, - accountId, - ]); - const onCreateAddress = useCallback(async () => { await refreshAccount(); void run(); @@ -620,82 +642,6 @@ const ProtocolDetailsPage = () => { const handleStake = useHandleStake(); const { handleSwap } = useHandleSwap(); - // const { result: trackingResp, run: refreshTracking } = usePromiseResult( - // async () => { - // if ( - // provider.toLowerCase() !== EEarnProviderEnum.Babylon.toLowerCase() || - // !earnAccount - // ) { - // return []; - // } - // const items = - // await backgroundApiProxy.serviceStaking.getBabylonTrackingItems({ - // accountId: earnAccount.accountId, - // networkId: earnAccount.networkId, - // }); - // return items; - // }, - // [provider, earnAccount], - // { initResult: [] }, - // ); - - // const isFocused = useIsFocused(); - // useEffect(() => { - // if (isFocused) { - // void refreshTracking(); - // } - // }, [isFocused, refreshTracking]); - - // const onRefreshTracking = useCallback(async () => { - // void run(); - // void refreshTracking(); - // }, [run, refreshTracking]); - - const protocolInfo: IProtocolInfo | undefined = useMemo(() => { - const withdrawAction = detailInfo?.actions?.find( - (i) => i.type === 'withdraw', - ) as IEarnWithdrawActionIcon; - return detailInfo?.protocol - ? { - ...detailInfo.protocol, - apyDetail: detailInfo.apyDetail, - earnAccount, - activeBalance: withdrawAction?.data?.balance, - eventEndTime: detailInfo?.countDownAlert?.endTime, - stakeTag: buildLocalTxStatusSyncId({ - providerName: provider, - tokenSymbol: symbol, - }), - - // withdraw - overflowBalance: detailInfo.nums?.overflow, - maxUnstakeAmount: detailInfo.nums?.maxUnstakeAmount, - minUnstakeAmount: detailInfo.nums?.minUnstakeAmount, - - // staking - minTransactionFee: detailInfo.nums?.minTransactionFee, - remainingCap: detailInfo.nums?.remainingCap, - - // claim - claimable: detailInfo.nums?.claimable, - } - : undefined; - }, [ - detailInfo?.actions, - detailInfo?.apyDetail, - detailInfo?.countDownAlert?.endTime, - detailInfo?.nums?.claimable, - detailInfo?.nums?.maxUnstakeAmount, - detailInfo?.nums?.minTransactionFee, - detailInfo?.nums?.minUnstakeAmount, - detailInfo?.nums?.remainingCap, - detailInfo?.nums?.overflow, - detailInfo?.protocol, - earnAccount, - provider, - symbol, - ]); - const onStake = useCallback(async () => { if ( earnAccount?.accountAddress && @@ -1222,7 +1168,9 @@ function ProtocolDetailsPageWithProvider() { }} enabledNum={[0]} > - + + + ); } diff --git a/packages/kit/src/views/Staking/router/index.tsx b/packages/kit/src/views/Staking/router/index.tsx index c918b720a33e..7e1a65b91f16 100644 --- a/packages/kit/src/views/Staking/router/index.tsx +++ b/packages/kit/src/views/Staking/router/index.tsx @@ -21,6 +21,10 @@ const ProtocolDetailsV2 = LazyLoad( () => import('@onekeyhq/kit/src/views/Staking/pages/ProtocolDetailsV2'), ); +const ManagePosition = LazyLoad( + () => import('@onekeyhq/kit/src/views/Staking/pages/ManagePosition'), +); + const Withdraw = LazyLoad( () => import('@onekeyhq/kit/src/views/Staking/pages/Withdraw'), ); @@ -75,6 +79,11 @@ export const StakingModalRouter: IModalFlowNavigatorConfig< exact: true, rewrite: '/defi/:network/:symbol/:provider', }, + { + name: EModalStakingRoutes.ManagePosition, + component: ManagePosition, + exact: true, + }, { name: EModalStakingRoutes.Stake, component: Stake, diff --git a/packages/shared/src/eventBus/appEventBus.ts b/packages/shared/src/eventBus/appEventBus.ts index 2014457b4e61..0bc0331fcfac 100644 --- a/packages/shared/src/eventBus/appEventBus.ts +++ b/packages/shared/src/eventBus/appEventBus.ts @@ -241,6 +241,12 @@ export interface IAppEventBusPayload { accountId: string; networkId: string; }; + [EAppEventBusNames.RefreshEarnPortfolioItem]: { + provider: string; + symbol: string; + networkId: string; + }; + [EAppEventBusNames.RefreshEarnPortfolio]: undefined; [EAppEventBusNames.AccountDataUpdate]: undefined; [EAppEventBusNames.AccountValueUpdate]: undefined; [EAppEventBusNames.onDragBeginInListView]: undefined; diff --git a/packages/shared/src/eventBus/appEventBusNames.ts b/packages/shared/src/eventBus/appEventBusNames.ts index 6091fa4d13f0..643abd6c961a 100644 --- a/packages/shared/src/eventBus/appEventBusNames.ts +++ b/packages/shared/src/eventBus/appEventBusNames.ts @@ -61,6 +61,8 @@ export enum EAppEventBusNames { RefreshHistoryList = 'RefreshHistoryList', RefreshBookmarkList = 'RefreshBookmarkList', RefreshApprovalList = 'RefreshApprovalList', + RefreshEarnPortfolioItem = 'RefreshEarnPortfolioItem', + RefreshEarnPortfolio = 'RefreshEarnPortfolio', AccountDataUpdate = 'AccountDataUpdate', AccountValueUpdate = 'AccountValueUpdate', onDragBeginInListView = 'onDragBeginInListView', diff --git a/packages/shared/src/locale/enum/translations.ts b/packages/shared/src/locale/enum/translations.ts index d439de1a69da..abe7707a88bb 100644 --- a/packages/shared/src/locale/enum/translations.ts +++ b/packages/shared/src/locale/enum/translations.ts @@ -163,6 +163,8 @@ auth_set_passcode = 'auth.set_passcode', auth_set_password = 'auth.set_password', auth_with_biometric = 'auth.with_biometric', + backing_up_desc = 'backing_up_desc', + backing_up_title = 'backing_up_title', backup_address_book_labels = 'backup.address_book_labels', backup_all_devices = 'backup.all_devices', backup_backup_deleted = 'backup.backup_deleted', @@ -213,6 +215,9 @@ backup_verify_app_password_to_import_data = 'backup.verify_app_password_to_import_data', backup_verify_apple_account_and_icloud_drive_enabled = 'backup.verify_apple_account_and_icloud_drive_enabled', backup_verify_google_account_and_google_drive_enabled = 'backup.verify_google_account_and_google_drive_enabled', + backup_restored = 'backup_restored', + backup_success_toast_title = 'backup_success_toast_title', + backup_write_to_cloud_failed = 'backup_write_to_cloud_failed', balance_detail_button_acknowledge = 'balance_detail.button_acknowledge', balance_detail_button_balance = 'balance_detail.button_balance', balance_detail_button_cancel = 'balance_detail.button_cancel', @@ -824,6 +829,7 @@ earn_withdrawal_take_up_to_number_days = 'earn.withdrawal_take_up_to_number_days', earn_withdrawal_up_to_number_days = 'earn.withdrawal_up_to_number_days', earn_withdrawn = 'earn.withdrawn', + earn_no_assets_deposited = 'earn_no_assets_deposited', earn_reward_distribution_schedule = 'earn_reward_distribution_schedule', edit_fee_custom_set_as_default_description = 'edit_fee_custom_set_as_default_description', energy_consumed = 'energy_consumed', @@ -1036,6 +1042,7 @@ firmware_update_status_transferring_data = 'firmware_update.status_transferring_data', firmware_update_status_validating = 'firmware_update.status_validating', for_reference_only = 'for_reference_only', + forgot_password_no_question_mark = 'forgot_password_no_question_mark', form_address_error_invalid = 'form.address_error_invalid', form_address_placeholder = 'form.address_placeholder', form_amount_placeholder = 'form.amount_placeholder', @@ -1116,6 +1123,7 @@ global_approve = 'global.approve', global_apr = 'global.apr', global_asset = 'global.asset', + global_at_least_variable_characters = 'global.at_least_variable_characters', global_auto = 'global.auto', global_available = 'global.available', global_backed_up = 'global.backed_up', @@ -1662,6 +1670,10 @@ global__multichain = 'global__multichain', global_add_money = 'global_add_money', global_buy_crypto = 'global_buy_crypto', + global_sign_in = 'global_sign_in', + google_account_not_signed_in = 'google_account_not_signed_in', + google_play_services_not_available_desc = 'google_play_services_not_available_desc', + google_play_services_not_available_title = 'google_play_services_not_available_title', hardware_backup_completed = 'hardware.backup_completed', hardware_bluetooth_need_turned_on_error = 'hardware.bluetooth_need_turned_on_error', hardware_bluetooth_not_paired_error = 'hardware.bluetooth_not_paired_error', @@ -1784,6 +1796,8 @@ id_refer_a_friend = 'id.refer_a_friend', id_refer_a_friend_desc = 'id.refer_a_friend_desc', identical_name_asset_alert = 'identical_name_asset_alert', + import_backup_password_desc = 'import_backup_password_desc', + import_hardware_phrases_warning = 'import_hardware_phrases_warning', import_phrase_or_private_key = 'import_phrase_or_private_key', insufficient_fee_append_desc = 'insufficient_fee_append_desc', interact_with_contract = 'interact_with_contract', @@ -1823,6 +1837,8 @@ ln_authorize_access_desc = 'ln.authorize_access_desc', ln_authorize_access_network_error = 'ln.authorize_access_network_error', ln_payment_received_label = 'ln.payment_received_label', + log_out_confirmation_text = 'log_out_confirmation_text', + logged_out_feedback = 'logged_out_feedback', login_forgot_passcode = 'login.forgot_passcode', login_forgot_password = 'login.forgot_password', login_welcome_message = 'login.welcome_message', @@ -1988,6 +2004,11 @@ nft_token_address = 'nft.token_address', nft_token_id = 'nft.token_id', no_account = 'no_account', + no_backup_found_google_desc = 'no_backup_found_google_desc', + no_backup_found_icloud_desc = 'no_backup_found_icloud_desc', + no_backup_found_no_wallet = 'no_backup_found_no_wallet', + no_backup_found_no_wallet_desc = 'no_backup_found_no_wallet_desc', + no_backups_found = 'no_backups_found', no_external_wallet_message = 'no_external_wallet_message', no_private_key_account_message = 'no_private_key_account_message', no_standard_wallet_desc = 'no_standard_wallet_desc', @@ -2022,6 +2043,7 @@ notifications_test_message_desc = 'notifications.test_message_desc', notifications_test_message_title = 'notifications.test_message_title', notifications_windows_notifications_permission_desc = 'notifications.windows_notifications_permission_desc', + older_backups_description = 'older_backups_description', onboarding_activate_device = 'onboarding.activate_device', onboarding_activate_device_all_set = 'onboarding.activate_device_all_set', onboarding_activate_device_by_restore = 'onboarding.activate_device_by_restore', @@ -2373,6 +2395,8 @@ perps_share_position_btn_save_img = 'perps.share_position_btn_save_img', perps_share_position_title = 'perps.share_position_title', pick_your_device = 'pick_your_device', + preparing_backup_desc = 'preparing_backup_desc', + preparing_backup_title = 'preparing_backup_title', prime_about_cloud_sync = 'prime.about_cloud_sync', prime_about_cloud_sync_description = 'prime.about_cloud_sync_description', prime_about_cloud_sync_included_data_title = 'prime.about_cloud_sync_included_data_title', @@ -2894,6 +2918,8 @@ send_token_selector_search_placeholder = 'send_token_selector.search_placeholder', send_token_selector_select_token = 'send_token_selector.select_token', sending_krc20_warning_text = 'sending_krc20_warning_text', + set_new_backup_password = 'set_new_backup_password', + set_new_backup_password_desc = 'set_new_backup_password_desc', setting_floating_icon = 'setting.floating_icon', setting_floating_icon_always_display = 'setting.floating_icon_always_display', setting_floating_icon_always_display_description = 'setting.floating_icon_always_display_description', @@ -3085,6 +3111,7 @@ signature_format_standard = 'signature_format_standard', signature_format_title = 'signature_format_title', signature_type_not_supported_on_model = 'signature_type_not_supported_on_model', + signed_in_feedback = 'signed_in_feedback', skip_firmware_check_dialog_desc = 'skip_firmware_check_dialog_desc', skip_firmware_check_dialog_title = 'skip_firmware_check_dialog_title', skip_verify_text = 'skip_verify_text', @@ -3619,7 +3646,9 @@ v4_migration_welcome_message = 'v4_migration.welcome_message', v4_migration_welcome_message_desc = 'v4_migration.welcome_message_desc', verify_backup_password = 'verify_backup_password', + verify_backup_password_desc = 'verify_backup_password_desc', verify_message_address_form_description = 'verify_message_address_form_description', + view_older_backups = 'view_older_backups', wallet_approval_alert_title_summary = 'wallet.approval_alert_title_summary', wallet_approval_approval_details = 'wallet.approval_approval_details', wallet_approval_approved_token = 'wallet.approval_approved_token', diff --git a/packages/shared/src/locale/json/bn.json b/packages/shared/src/locale/json/bn.json index c5738ef7d5ac..bada94449beb 100644 --- a/packages/shared/src/locale/json/bn.json +++ b/packages/shared/src/locale/json/bn.json @@ -158,6 +158,8 @@ "auth.set_passcode": "পাসকোড সেট করুন", "auth.set_password": "পাসওয়ার্ড সেট করুন", "auth.with_biometric": "{biometric} দ্বারা প্রমাণীকরণ", + "backing_up_desc": "ব্যাকআপ সম্পূর্ণ না হওয়া পর্যন্ত এই উইন্ডোটি বন্ধ করবেন না।", + "backing_up_title": "ব্যাকআপ নেওয়া হচ্ছে…", "backup.address_book_labels": "ঠিকানা বই & লেবেল", "backup.all_devices": "সমস্ত যন্ত্রপাতি", "backup.backup_deleted": "ব্যাকআপ মুছে ফেলা হয়েছে", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "ডেটা আমদানিতে ব্যাকআপ করার সময় অ্যাপ পাসওয়ার্ড যাচাই করুন।", "backup.verify_apple_account_and_icloud_drive_enabled": "দয়া করে আপনার অ্যাপল অ্যাকাউন্ট লগইন যাচাই করুন এবং নিশ্চিত করুন যে iCloud Drive সক্রিয় এবং OneKey এর জন্য অনুমোদিত।", "backup.verify_google_account_and_google_drive_enabled": "দয়া করে আপনার Google অ্যাকাউন্ট লগইন যাচাই করুন এবং নিশ্চিত করুন যে Google Drive সক্রিয় এবং OneKey এর জন্য অনুমোদিত।", + "backup_restored": "ব্যাকআপ পুনরুদ্ধার হয়েছে!", + "backup_success_toast_title": "সব প্রস্তুত! আপনার ব্যাকআপ সম্পন্ন হয়েছে", + "backup_write_to_cloud_failed": "উফ! এইবার ব্যাকআপ ব্যর্থ হয়েছে। দয়া করে আবার চেষ্টা করুন", "balance_detail.button_acknowledge": "স্বীকার করা", "balance_detail.button_balance": "ব্যালেন্সের বিস্তারিত", "balance_detail.button_cancel": "বাতিল করুন", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "প্রত্যাহারের অনুরোধ জানানোর পর {number} দিন সময় নিন", "earn.withdrawal_up_to_number_days": "উত্তোলন {number} দিন পর্যন্ত সময় নিতে পারে, এরপর আপনার স্টেকড সম্পদগুলি উপলব্ধ হবে", "earn.withdrawn": "প্রত্যাহার করা হয়েছে", + "earn_no_assets_deposited": "এখনও কোনও সম্পদ জমা দেওয়া হয়নি।", "earn_reward_distribution_schedule": "আগামী মাসের ১০ তারিখের মধ্যে আপনার Arbitrum ওয়ালেটে (Ethereum এর ঠিকানায়) পুরষ্কার বিতরণ করা হবে।", "edit_fee_custom_set_as_default_description": "সমস্ত ভবিষ্যৎ লেনদেনের জন্য {network}-এ ডিফল্ট হিসাবে সেট করুন", "energy_consumed": "ব্যবহৃত শক্তি", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "ডেটা স্থানান্তর করা", "firmware_update.status_validating": "যাচাই করা হচ্ছে", "for_reference_only": "শুধুমাত্র রেফারেন্সের জন্য", + "forgot_password_no_question_mark": "পাসওয়ার্ড ভুলে গেছেন", "form.address_error_invalid": "অবৈধ ঠিকানা", "form.address_placeholder": "ঠিকানা বা ডোমেইন", "form.amount_placeholder": "পরিমাণ প্রবেশ করুন", @@ -1101,7 +1108,7 @@ "global.allow": "অনুমতি দিন", "global.always": "সর্বদা", "global.an_error_occurred": "একটি ত্রুটি ঘটেছে", - "global.an_error_occurred_desc": "আমরা আপনার অনুরোধটি সম্পন্ন করতে অক্ষম। দয়া করে কয়েক মিনিটের মধ্যে পৃষ্ঠাটি রিফ্রেশ করুন।", + "global.an_error_occurred_desc": "রিফ্রেশ করুন এবং কয়েক মিনিটের মধ্যে আবার চেষ্টা করুন।", "global.app_wallet": "অ্যাপ ওয়ালেট", "global.apply": "আবেদন করুন", "global.approval": "অনুমোদন", @@ -1111,6 +1118,7 @@ "global.approve": "অনুমোদন করুন", "global.apr": "APR", "global.asset": "সম্পত্তি", + "global.at_least_variable_characters": "কমপক্ষে {variable} অক্ষর", "global.auto": "অটো", "global.available": "উপলব্ধ", "global.backed_up": "ব্যাকআপ করা হয়েছে", @@ -1657,6 +1665,10 @@ "global__multichain": "মাল্টিচেইন", "global_add_money": "টাকা যোগ করুন", "global_buy_crypto": "ক্রিপ্টো কিনুন", + "global_sign_in": "সাইন ইন", + "google_account_not_signed_in": "Google অ্যাকাউন্টে সাইন ইন করা হয়নি", + "google_play_services_not_available_desc": "দয়া করে Google Play Services ইন্সটল করে আপনার Google অ্যাকাউন্ট দিয়ে সাইন ইন করুন।", + "google_play_services_not_available_title": "Google Play Services উপলভ্য নয়", "hardware.backup_completed": "ব্যাকআপ সম্পন্ন হয়েছে!", "hardware.bluetooth_need_turned_on_error": "ব্লুটুথ বন্ধ আছে", "hardware.bluetooth_not_paired_error": "ব্লুটুথ জোড়া হয়নি", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "একজন বন্ধুকে উল্লেখ করুন", "id.refer_a_friend_desc": "বন্ধুদের আমন্ত্রণ জানান পুরস্কার অর্জনের জন্য", "identical_name_asset_alert": "আমরা আপনার ওয়ালেটে একই রকম নামের সম্পদ সনাক্ত করেছি। সতর্কতার সাথে এগিয়ে যান।", + "import_backup_password_desc": "অনুগ্রহ করে এই ব্যাকআপের জন্য পাসওয়ার্ড লিখুন।", + "import_hardware_phrases_warning": "আপনার হার্ডওয়্যার ওয়ালেটের রিকভারি ফ্রেজ ইম্পোর্ট করবেন না। হার্ডওয়্যার ওয়ালেট সংযুক্ত করুন ↗ বরং করুন", "import_phrase_or_private_key": "ফ্রেজ বা প্রাইভেট কী ইমপোর্ট করুন", "insufficient_fee_append_desc": "সর্বাধিক আনুমানিক ফি ভিত্তিক: {amount} {symbol}", "interact_with_contract": "(এর সাথে) যোগাযোগ করুন (প্রতি)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "আপনার হার্ডওয়্যার ওয়ালেট সংযোগ করে লাইটনিং অ্যাকাউন্ট অ্যাক্সেস করুন", "ln.authorize_access_network_error": "প্রমাণীকরণ ব্যর্থ হয়েছে, আপনার নেটওয়ার্ক সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন", "ln.payment_received_label": "পেমেন্ট পেয়েছি", + "log_out_confirmation_text": "আপনি কি নিশ্চিত যে আপনি ${email} থেকে লগ আউট করতে চান?", + "logged_out_feedback": "সফলভাবে লগ আউট হয়েছে", "login.forgot_passcode": "পাসকোড ভুলে গেছেন?", "login.forgot_password": "পাসওয়ার্ড ভুলে গেছেন?", "login.welcome_message": "ফিরে আসার জন্য স্বাগতম", @@ -1983,6 +1999,11 @@ "nft.token_address": "টোকেন ঠিকানা", "nft.token_id": "টোকেন আইডি", "no_account": "কোন অ্যাকাউন্ট নেই", + "no_backup_found_google_desc": "অনুগ্রহ করে আপনার Google Drive সাইন-ইন যাচাই করুন, নিশ্চিত করুন আপনি সঠিক Google অ্যাকাউন্ট ব্যবহার করছেন, অথবা আপনার নেটওয়ার্ক সংযোগ পরীক্ষা করুন", + "no_backup_found_icloud_desc": "অনুগ্রহ করে আপনার iCloud সাইন-ইন যাচাই করুন, iCloud এবং কীচেইন সিঙ্ক চালু করুন, অথবা আপনার নেটওয়ার্ক সংযোগ পরীক্ষা করুন", + "no_backup_found_no_wallet": "ব্যাকআপ করার জন্য কোনো ওয়ালেট নেই", + "no_backup_found_no_wallet_desc": "অনুগ্রহ করে প্রথমে একটি ওয়ালেট তৈরি করুন", + "no_backups_found": "কোনো ব্যাকআপ পাওয়া যায়নি", "no_external_wallet_message": "কোন বাইরের ওয়ালেট সংযুক্ত নেই। এখানে দেখার জন্য একটি তৃতীয় পক্ষের ওয়ালেট লিঙ্ক করুন।", "no_private_key_account_message": "কোন ব্যক্তিগত কী অ্যাকাউন্ট নেই। আপনার সম্পত্তি পরিচালনা করার জন্য একটি নতুন অ্যাকাউন্ট যোগ করুন।", "no_standard_wallet_desc": "এখনও কোনো স্ট্যান্ডার্ড ওয়ালেট নেই", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "আপনি রিয়েল-টাইম অ্যাকাউন্ট আপডেট, নিরাপত্তা সতর্কতা এবং আরও গুরুত্বপূর্ণ তথ্য পাবেন।", "notifications.test_message_title": "পুশ নোটিফিকেশনগুলি প্রস্তুত!", "notifications.windows_notifications_permission_desc": "অ্যাপ এবং অন্যান্য প্রেরকদের কাছ থেকে বিজ্ঞপ্তিগুলি পান", + "older_backups_description": "এই ব্যাকআপগুলো আগের সংস্করণগুলোতে তৈরি করা হয়েছিল। {version} থেকে, ওয়ালেট ব্যাকআপ ম্যানুয়াল, এবং ঠিকানা বইয়ের ডেটা এখন ক্লাউড ব্যাকআপের বদলে OneKey Cloud Sync-এর অংশ।", "onboarding.activate_device": "আপনার ডিভাইস সক্রিয় করুন", "onboarding.activate_device_all_set": "সব প্রস্তুত!", "onboarding.activate_device_by_restore": "ওয়ালেট পুনরুদ্ধার করুন", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "ছবি সংরক্ষণ করুন", "perps.share_position_title": "পজিশন শেয়ার করুন", "pick_your_device": "আপনার ডিভাইস নির্বাচন করুন", + "preparing_backup_desc": "একটু অপেক্ষা করুন…", + "preparing_backup_title": "ব্যাকআপ প্রস্তুত করা হচ্ছে…", "prime.about_cloud_sync": "ক্লাউড সিঙ্ক সম্বন্ধে", "prime.about_cloud_sync_description": "জানুন কোন ডেটা সিঙ্ক করা হয় এবং আপনার গোপনীয়তা কীভাবে সুরক্ষিত রাখা হয়।", "prime.about_cloud_sync_included_data_title": "অন্তর্ভুক্ত ডেটা", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "সিম্বল বা কন্ট্রাক্ট ঠিকানা অনুসন্ধান করুন", "send_token_selector.select_token": "টোকেন নির্বাচন করুন", "sending_krc20_warning_text": "KRC20 টোকেন পাঠাতে আপনার হার্ডওয়্যারে দুটি নিশ্চিতকরণ প্রয়োজন। দ্বিতীয় নিশ্চিতকরণ বাতিল করবেন না, নতুবা প্রথম নিশ্চিতকরণে পাঠানো KAS পুনরুদ্ধার করা কঠিন হবে।", + "set_new_backup_password": "নতুন ব্যাকআপ পাসওয়ার্ড সেট করুন", + "set_new_backup_password_desc": "নতুন ব্যাকআপ পাসওয়ার্ডটি আপনার পরবর্তী ব্যাকআপ রেকর্ডের জন্য ব্যবহার করা হবে। বিদ্যমান ব্যাকআপ রেকর্ড এবং তাদের পাসওয়ার্ড একই থাকবে।", "setting.floating_icon": "ভাসমান আইকন", "setting.floating_icon_always_display": "সবসময় প্রদর্শন করুন", "setting.floating_icon_always_display_description": "সক্রিয় করা হলে, OneKey আইকনটি ওয়েবপেজের প্রান্তে ভাসে, যা আপনাকে দ্রুত dApps-এর নিরাপত্তা তথ্য পরীক্ষা করতে সহায়তা করে।", @@ -3080,6 +3106,7 @@ "signature_format_standard": "স্ট্যান্ডার্ড (ইলেকট্রাম) — বেশিরভাগ ওয়ালেট এবং পরিষেবার সাথে কাজ করে", "signature_format_title": "স্বাক্ষরের ফরম্যাট", "signature_type_not_supported_on_model": "{deviceModel} ডিভাইসে {sigType} সিগনেচার ফরম্যাটটি সমর্থিত নয়", + "signed_in_feedback": "সফলভাবে সাইন ইন হয়েছে", "skip_firmware_check_dialog_desc": "আপনি কি নিশ্চিত আপনি চেকটি এড়িয়ে যেতে চান? সর্বশেষ ফার্মওয়্যার ব্যবহার করলে আপনি সর্বোচ্চ সুরক্ষা পান।", "skip_firmware_check_dialog_title": "ফার্মওয়্যার পরীক্ষা এড়িয়ে যাবেন?", "skip_verify_text": "আমার ডিভাইসটি আমার সাথে নেই", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "আপডেটের সময় আপনার অ্যাপ বন্ধ করবেন না", "v4_migration.welcome_message": "OneKey 5.0 এখন এখানে!", "v4_migration.welcome_message_desc": "এখানে আপনার ডাটা নিরাপদে এবং দ্রুত স্থানান্তর করার উপায় রয়েছে। শুরু করার জন্য প্রস্তুত?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "ব্যাকআপ পাসওয়ার্ড যাচাই করুন", + "verify_backup_password_desc": "অগ্রসর হতে আপনার ক্লাউড ব্যাকআপ পাসওয়ার্ডটি নিশ্চিত করুন। যদি আপনি এটি ভুলে যান, তাহলে “পাসওয়ার্ড ভুলে গেছেন” ব্যবহার করে রিসেট করতে পারেন।", "verify_message_address_form_description": "সমর্থিত: {networks}", + "view_older_backups": "পুরোনো ব্যাকআপগুলো দেখুন", "wallet.approval_alert_title_summary": "{riskyNumber}টি ঝুঁকিপূর্ণ অনুমোদন এবং {inactiveNumber}টি নিষ্ক্রিয় চুক্তি শনাক্ত করা হয়েছে", "wallet.approval_approval_details": "অনুমোদনের বিস্তারিত", "wallet.approval_approved_token": "অনুমোদিত টোকেন", diff --git a/packages/shared/src/locale/json/de.json b/packages/shared/src/locale/json/de.json index 9ad6de4e8c06..64a1f81e5495 100644 --- a/packages/shared/src/locale/json/de.json +++ b/packages/shared/src/locale/json/de.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Passcode festlegen", "auth.set_password": "Passwort festlegen", "auth.with_biometric": "Authentifizierung mit {biometric}", + "backing_up_desc": "Schließen Sie dieses Fenster nicht, bis die Sicherung abgeschlossen ist.", + "backing_up_title": "Sicherung wird erstellt …", "backup.address_book_labels": "Adressbuch & Etiketten", "backup.all_devices": "Alle Geräte", "backup.backup_deleted": "Sicherung gelöscht", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Überprüfen Sie beim Sichern das App-Passwort, um Daten zu importieren.", "backup.verify_apple_account_and_icloud_drive_enabled": "Bitte überprüfen Sie Ihre Apple-Kontoanmeldung und stellen Sie sicher, dass iCloud Drive aktiviert und für OneKey autorisiert ist.", "backup.verify_google_account_and_google_drive_enabled": "Bitte überprüfen Sie Ihre Google-Konto-Anmeldung und stellen Sie sicher, dass Google Drive aktiviert und für OneKey autorisiert ist.", + "backup_restored": "Backup wiederhergestellt!", + "backup_success_toast_title": "Alles erledigt! Dein Backup ist abgeschlossen", + "backup_write_to_cloud_failed": "Ups! Das Backup ist diesmal fehlgeschlagen. Bitte versuche es erneut", "balance_detail.button_acknowledge": "Anerkennen", "balance_detail.button_balance": "Kontodetails", "balance_detail.button_cancel": "Abbrechen", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Nehmen Sie {number} Tage nach dem Stellen eines Auszahlungsantrags", "earn.withdrawal_up_to_number_days": "Die Auszahlung kann bis zu {number} Tage dauern, danach stehen Ihre eingesetzten Vermögenswerte zur Verfügung", "earn.withdrawn": "Zurückgezogen", + "earn_no_assets_deposited": "Bisher wurden keine Vermögenswerte eingezahlt.", "earn_reward_distribution_schedule": "Die Belohnungen werden bis zum 10. des nächsten Monats an Ihr Arbitrum-Wallet (dieselbe Adresse wie Ethereum) verteilt.", "edit_fee_custom_set_as_default_description": "Als Standard für alle zukünftigen Transaktionen auf {network} festlegen", "energy_consumed": "Verbrauchte Energie", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Datenübertragung", "firmware_update.status_validating": "Validierung", "for_reference_only": "Nur zur Referenz", + "forgot_password_no_question_mark": "Passwort vergessen", "form.address_error_invalid": "Ungültige Adresse", "form.address_placeholder": "Adresse oder Domain", "form.amount_placeholder": "Geben Sie den Betrag ein", @@ -1101,7 +1108,7 @@ "global.allow": "Erlauben", "global.always": "Immer", "global.an_error_occurred": "Ein Fehler ist aufgetreten", - "global.an_error_occurred_desc": "Wir können Ihre Anfrage nicht bearbeiten. Bitte aktualisieren Sie die Seite in ein paar Minuten.", + "global.an_error_occurred_desc": "Bitte aktualisieren Sie die Seite und versuchen Sie es in wenigen Minuten erneut.", "global.app_wallet": "App Wallet", "global.apply": "Anwenden", "global.approval": "Genehmigung", @@ -1111,6 +1118,7 @@ "global.approve": "Genehmigen", "global.apr": "APR", "global.asset": "Vermögen", + "global.at_least_variable_characters": "Mindestens {variable} Zeichen", "global.auto": "Auto", "global.available": "Verfügbar", "global.backed_up": "Gesichert", @@ -1657,6 +1665,10 @@ "global__multichain": "Mehrkette", "global_add_money": "Geld hinzufügen", "global_buy_crypto": "Krypto kaufen", + "global_sign_in": "Anmelden", + "google_account_not_signed_in": "Google-Konto nicht angemeldet", + "google_play_services_not_available_desc": "Bitte installiere Google Play-Dienste und melde dich mit deinem Google-Konto an.", + "google_play_services_not_available_title": "Google Play-Dienste sind nicht verfügbar", "hardware.backup_completed": "Sicherung abgeschlossen!", "hardware.bluetooth_need_turned_on_error": "Bluetooth ist aus", "hardware.bluetooth_not_paired_error": "Bluetooth nicht gekoppelt", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Einen Freund empfehlen", "id.refer_a_friend_desc": "Laden Sie Freunde ein, um Belohnungen zu verdienen", "identical_name_asset_alert": "Wir haben Vermögenswerte mit ähnlichen Namen in deiner Wallet erkannt. Bitte sei vorsichtig.", + "import_backup_password_desc": "Bitte geben Sie das Passwort für dieses Backup ein.", + "import_hardware_phrases_warning": "Importiere nicht die Wiederherstellungsphrase deiner Hardware-Wallet. Hardware-Wallet verbinden ↗ stattdessen", "import_phrase_or_private_key": "Phrase oder privaten Schlüssel importieren", "insufficient_fee_append_desc": "basierend auf der maximal geschätzten Gebühr: {amount} {symbol}", "interact_with_contract": "Interagieren mit (An)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Verbinden Sie Ihre Hardware-Wallet, um auf das Lightning-Konto zuzugreifen", "ln.authorize_access_network_error": "Authentifizierung fehlgeschlagen, überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut", "ln.payment_received_label": "Zahlung erhalten", + "log_out_confirmation_text": "Möchten Sie sich wirklich von ${email} abmelden?", + "logged_out_feedback": "Erfolgreich abgemeldet", "login.forgot_passcode": "Passcode vergessen?", "login.forgot_password": "Passwort vergessen?", "login.welcome_message": "Willkommen zurück", @@ -1983,6 +1999,11 @@ "nft.token_address": "Token-Adresse", "nft.token_id": "Token-ID", "no_account": "Kein Konto", + "no_backup_found_google_desc": "Bitte überprüfen Sie Ihre Google Drive-Anmeldung, stellen Sie sicher, dass Sie das richtige Google-Konto verwenden, oder prüfen Sie Ihre Netzwerkverbindung", + "no_backup_found_icloud_desc": "Bitte überprüfen Sie Ihre iCloud-Anmeldung, aktivieren Sie iCloud und die Schlüsselbund-Synchronisierung, oder überprüfen Sie Ihre Netzwerkverbindung", + "no_backup_found_no_wallet": "Keine Wallets zum Sichern verfügbar", + "no_backup_found_no_wallet_desc": "Bitte erstellen Sie zuerst eine Wallet", + "no_backups_found": "Keine Backups gefunden", "no_external_wallet_message": "Es sind keine externen Wallets verbunden. Verknüpfen Sie eine Wallet eines Drittanbieters, um sie hier anzuzeigen.", "no_private_key_account_message": "Keine privaten Schlüsselkonten. Fügen Sie ein neues Konto hinzu, um Ihre Vermögenswerte zu verwalten.", "no_standard_wallet_desc": "Noch keine Standard-Wallet", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Sie erhalten Echtzeit-Kontoaktualisierungen, Sicherheitswarnungen und weitere wichtige Informationen.", "notifications.test_message_title": "Push-Benachrichtigungen sind bereit!", "notifications.windows_notifications_permission_desc": "Erhalten Sie Benachrichtigungen von Apps und anderen Absendern", + "older_backups_description": "Diese Backups wurden in früheren Versionen erstellt. Seit {version} sind Wallet-Backups manuell, und Adressbuchdaten sind jetzt Teil von OneKey Cloud Sync statt der Cloud-Backups.", "onboarding.activate_device": "Aktivieren Sie Ihr Gerät", "onboarding.activate_device_all_set": "Alles bereit!", "onboarding.activate_device_by_restore": "Wallet wiederherstellen", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Bild speichern", "perps.share_position_title": "Position teilen", "pick_your_device": "Wähle dein Gerät aus", + "preparing_backup_desc": "Einen Moment …", + "preparing_backup_title": "Sicherung wird vorbereitet …", "prime.about_cloud_sync": "Über die Cloud-Synchronisation", "prime.about_cloud_sync_description": "Erfahren Sie, welche Daten synchronisiert und wie Ihre Privatsphäre geschützt wird.", "prime.about_cloud_sync_included_data_title": "Synchronisierte Daten", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Suchen Sie Symbol oder Vertragsadresse", "send_token_selector.select_token": "Token auswählen", "sending_krc20_warning_text": "Das Senden von KRC20-Token erfordert zwei Bestätigungen auf Ihrer Hardware. Brechen Sie die zweite Bestätigung nicht ab, da sonst die im ersten Bestätigungsschritt gesendeten KAS nur schwer wiederherzustellen sind.", + "set_new_backup_password": "Neues Backup-Passwort festlegen", + "set_new_backup_password_desc": "Das neue Backup-Passwort wird für Ihren nächsten Sicherungseintrag verwendet. Vorhandene Sicherungseinträge und deren Passwörter bleiben unverändert.", "setting.floating_icon": "Schwebendes Symbol", "setting.floating_icon_always_display": "Immer anzeigen", "setting.floating_icon_always_display_description": "Wenn aktiviert, schwebt das OneKey-Symbol am Rand der Webseite und hilft Ihnen, die Sicherheitsinformationen von dApps schnell zu überprüfen.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Standard (Electrum) — Funktioniert mit den meisten Wallets und Diensten", "signature_format_title": "Signaturformat", "signature_type_not_supported_on_model": "{sigType} wird auf dem Gerät {deviceModel} nicht unterstützt.", + "signed_in_feedback": "Erfolgreich angemeldet", "skip_firmware_check_dialog_desc": "Möchten Sie die Prüfung wirklich überspringen? Mit aktueller Firmware sind Sie am besten geschützt.", "skip_firmware_check_dialog_title": "Firmware-Prüfung überspringen?", "skip_verify_text": "Ich habe mein Gerät nicht bei mir", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Schließen Sie Ihre App während des Updates nicht", "v4_migration.welcome_message": "OneKey 5.0 ist da!", "v4_migration.welcome_message_desc": "Hier erfahren Sie, wie Sie Ihre Daten sicher und schnell migrieren können. Sind Sie bereit zu beginnen?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Backup-Passwort bestätigen", + "verify_backup_password_desc": "Bitte bestätigen Sie Ihr Cloud-Backup-Passwort, um fortzufahren. Wenn Sie es vergessen haben, können Sie es über „Passwort vergessen“ zurücksetzen.", "verify_message_address_form_description": "Unterstützt: {networks}", + "view_older_backups": "Ältere Backups ansehen", "wallet.approval_alert_title_summary": "{riskyNumber} riskante Genehmigungen und {inactiveNumber} inaktive Verträge erkannt", "wallet.approval_approval_details": "Autorisierungsdetails", "wallet.approval_approved_token": "Genehmigtes Token", diff --git a/packages/shared/src/locale/json/en_US.json b/packages/shared/src/locale/json/en_US.json index c41c3971b2c2..49b1406241d6 100644 --- a/packages/shared/src/locale/json/en_US.json +++ b/packages/shared/src/locale/json/en_US.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Set passcode", "auth.set_password": "Set password", "auth.with_biometric": "Authentication with {biometric}", + "backing_up_desc": "Don’t close this window until the backup is complete.", + "backing_up_title": "Backing up…", "backup.address_book_labels": "Address book & labels", "backup.all_devices": "All devices", "backup.backup_deleted": "Backup deleted", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Verify the App password at the time of backup to Import data.", "backup.verify_apple_account_and_icloud_drive_enabled": "Please verify your Apple account login and ensure iCloud Drive is enabled and authorized for OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Please verify your Google account login and ensure Google Drive is enabled and authorized for OneKey.", + "backup_restored": "Backup restored!", + "backup_success_toast_title": "All set! Your backup is complete", + "backup_write_to_cloud_failed": "Oops! Backup failed this time. Please try again", "balance_detail.button_acknowledge": "Acknowledge", "balance_detail.button_balance": "Balance Details", "balance_detail.button_cancel": "Cancel", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Take {number} days after issuing a withdrawal request", "earn.withdrawal_up_to_number_days": "Withdrawal can take up to {number} days, and then your staked assets will be available", "earn.withdrawn": "Withdrawn", + "earn_no_assets_deposited": "No assets deposited yet", "earn_reward_distribution_schedule": "Rewards will be distributed to your Arbitrum wallet (same address as Ethereum) by the 10th of next month.", "edit_fee_custom_set_as_default_description": "Set as default for all future transactions on {network}", "energy_consumed": "Energy consumed", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Transferring data", "firmware_update.status_validating": "Validating", "for_reference_only": "Reference only", + "forgot_password_no_question_mark": "Forgot password", "form.address_error_invalid": "Invalid address", "form.address_placeholder": "Address or domain", "form.amount_placeholder": "Enter amount", @@ -1101,7 +1108,7 @@ "global.allow": "Allow", "global.always": "Always", "global.an_error_occurred": "An error occurred", - "global.an_error_occurred_desc": "We’re unable to complete your request. Please reload the page.", + "global.an_error_occurred_desc": "Please refresh and try again in a few minutes.", "global.app_wallet": "App wallet", "global.apply": "Apply", "global.approval": "Approval", @@ -1111,6 +1118,7 @@ "global.approve": "Approve", "global.apr": "APR", "global.asset": "Asset", + "global.at_least_variable_characters": "At least {variable} characters", "global.auto": "Auto", "global.available": "Available", "global.backed_up": "Backed up", @@ -1657,6 +1665,10 @@ "global__multichain": "Multichain", "global_add_money": "Add money", "global_buy_crypto": "Buy crypto", + "global_sign_in": "Sign in", + "google_account_not_signed_in": "Google account not signed in", + "google_play_services_not_available_desc": "Please install Google Play Services and sign in with your Google account.", + "google_play_services_not_available_title": "Google Play Services isn’t available", "hardware.backup_completed": "Backup completed!", "hardware.bluetooth_need_turned_on_error": "Bluetooth is off", "hardware.bluetooth_not_paired_error": "Bluetooth not paired", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Refer a Friend", "id.refer_a_friend_desc": "Invite friends to earn rewards", "identical_name_asset_alert": "We've detected assets with similar names in your wallet. Proceed with caution.", + "import_backup_password_desc": "Please enter the password for this backup.", + "import_hardware_phrases_warning": "Don’t import your hardware wallet’s recovery phrase. Connect Hardware Wallet ↗ instead", "import_phrase_or_private_key": "Import phrase or private key", "insufficient_fee_append_desc": "based on max est. fee: {amount} {symbol}", "interact_with_contract": "Interact with (To)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Connecting your hardware wallet to access the Lightning account", "ln.authorize_access_network_error": "Authentication failed, check your network connection and try again", "ln.payment_received_label": "Payment received", + "log_out_confirmation_text": "Are you sure you want to log out of ${email}?", + "logged_out_feedback": "Logged out successfully", "login.forgot_passcode": "Forgot passcode?", "login.forgot_password": "Forgot password?", "login.welcome_message": "Welcome back", @@ -1983,6 +1999,11 @@ "nft.token_address": "Token Address", "nft.token_id": "Token ID", "no_account": "No account", + "no_backup_found_google_desc": "Please verify your Google Drive sign-in, make sure you’re using the correct Google account, or check your network connection", + "no_backup_found_icloud_desc": "Please verify your iCloud sign-in, enable iCloud and Keychain sync, or check your network connection", + "no_backup_found_no_wallet": "No wallets available to back up", + "no_backup_found_no_wallet_desc": "Please create a wallet first", + "no_backups_found": "No backups found", "no_external_wallet_message": "No external wallets are connected. Link a third-party wallet to view here.", "no_private_key_account_message": "No private key accounts. Add a new account to manage your assets.", "no_standard_wallet_desc": "No standard wallet yet", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "You’ll receive real-time account updates, security alerts, and more important info.", "notifications.test_message_title": "Push notifications are ready!", "notifications.windows_notifications_permission_desc": "Get notifications from apps and other senders", + "older_backups_description": "These backups were created in earlier versions. Since {version}, wallet backups are manual, and address book data is now part of OneKey Cloud Sync instead of cloud backups.", "onboarding.activate_device": "Activate your device", "onboarding.activate_device_all_set": "All set!", "onboarding.activate_device_by_restore": "Restore wallet", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Save Image", "perps.share_position_title": "Share Position", "pick_your_device": "Pick your device", + "preparing_backup_desc": "Just a moment…", + "preparing_backup_title": "Preparing backup…", "prime.about_cloud_sync": "About cloud sync", "prime.about_cloud_sync_description": "Learn what data is synced and how your privacy is protected.", "prime.about_cloud_sync_included_data_title": "Included data", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Search symbol or contract address", "send_token_selector.select_token": "Select Token", "sending_krc20_warning_text": "Sending KRC20 tokens requires two confirmations on your hardware. Do not cancel the second confirmation, or the KAS sent in the first confirmation will be difficult to recover.", + "set_new_backup_password": "Set new backup password", + "set_new_backup_password_desc": "The new backup password is used for your next backup record. Existing backup records and their passwords will stay the same.", "setting.floating_icon": "Floating icon", "setting.floating_icon_always_display": "Always display", "setting.floating_icon_always_display_description": "When enabled, the OneKey icon floats on the webpage edge, helping you quickly check dApps' security information.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Standard (Electrum) — Works with most wallets and services", "signature_format_title": "Signature format", "signature_type_not_supported_on_model": "{sigType} is not supported on device: {deviceModel}", + "signed_in_feedback": "Signed in successfully", "skip_firmware_check_dialog_desc": "Are you sure you want to skip the check? Using up-to-date firmware gives you the best protection.", "skip_firmware_check_dialog_title": "Skip firmware check?", "skip_verify_text": "I don't have my device with me", @@ -3614,7 +3641,9 @@ "v4_migration.welcome_message": "OneKey 5.0 is here!", "v4_migration.welcome_message_desc": "Here’s how to securely and quickly migrate your data. Ready to start?", "verify_backup_password": "Verify backup password", + "verify_backup_password_desc": "Please confirm your cloud backup password to proceed. If you’ve forgotten it, you can reset it using Forgot password.", "verify_message_address_form_description": "Supports: {networks}", + "view_older_backups": "View older backups", "wallet.approval_alert_title_summary": "Detected {riskyNumber} risky approvals and {inactiveNumber} inactive contracts", "wallet.approval_approval_details": "Approval details", "wallet.approval_approved_token": "Approved token", diff --git a/packages/shared/src/locale/json/es.json b/packages/shared/src/locale/json/es.json index 415e913d15f3..12a0f2f47c27 100644 --- a/packages/shared/src/locale/json/es.json +++ b/packages/shared/src/locale/json/es.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Establecer código de acceso", "auth.set_password": "Establecer contraseña", "auth.with_biometric": "Autenticación con {biometric}", + "backing_up_desc": "No cierres esta ventana hasta que se complete la copia de seguridad.", + "backing_up_title": "Haciendo copia de seguridad…", "backup.address_book_labels": "Libreta de direcciones & etiquetas", "backup.all_devices": "Todos los dispositivos", "backup.backup_deleted": "Respaldo eliminado", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Verifique la contraseña de la aplicación al momento de realizar la copia de seguridad para importar datos.", "backup.verify_apple_account_and_icloud_drive_enabled": "Por favor, verifica tu inicio de sesión de Apple y asegúrate de que iCloud Drive está habilitado y autorizado para OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Por favor, verifica tu inicio de sesión en la cuenta de Google y asegúrate de que Google Drive está habilitado y autorizado para OneKey.", + "backup_restored": "¡Copia de seguridad restaurada!", + "backup_success_toast_title": "¡Listo! Tu copia de seguridad está completa", + "backup_write_to_cloud_failed": "¡Vaya! La copia de seguridad falló esta vez. Inténtalo de nuevo, por favor", "balance_detail.button_acknowledge": "Reconocer", "balance_detail.button_balance": "Detalles del Saldo", "balance_detail.button_cancel": "Cancelar", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Tome {number} días después de emitir una solicitud de retiro", "earn.withdrawal_up_to_number_days": "La retirada puede tardar hasta {number} días, y luego tus activos apostados estarán disponibles", "earn.withdrawn": "Retirado", + "earn_no_assets_deposited": "Aún no se han depositado activos.", "earn_reward_distribution_schedule": "Las recompensas se distribuirán a su billetera Arbitrum (la misma dirección que Ethereum) el día 10 del próximo mes.", "edit_fee_custom_set_as_default_description": "Establecer como predeterminado para todas las transacciones futuras en {network}", "energy_consumed": "Energía consumida", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Transfiriendo datos", "firmware_update.status_validating": "Validando", "for_reference_only": "Solo para referencia", + "forgot_password_no_question_mark": "¿Olvidaste tu contraseña?", "form.address_error_invalid": "Dirección inválida", "form.address_placeholder": "Dirección o dominio", "form.amount_placeholder": "Ingrese la cantidad", @@ -1101,7 +1108,7 @@ "global.allow": "Permitir", "global.always": "Siempre", "global.an_error_occurred": "Ocurrió un error", - "global.an_error_occurred_desc": "No podemos completar tu solicitud. Por favor, actualiza la página en unos minutos.", + "global.an_error_occurred_desc": "Por favor, actualice la página e inténtelo de nuevo en unos minutos.", "global.app_wallet": "Billetera de aplicación", "global.apply": "Aplicar", "global.approval": "Aprobación", @@ -1111,6 +1118,7 @@ "global.approve": "Aprobar", "global.apr": "APR", "global.asset": "Activo", + "global.at_least_variable_characters": "Al menos {variable} caracteres", "global.auto": "Auto", "global.available": "Disponible", "global.backed_up": "Respaldado", @@ -1657,6 +1665,10 @@ "global__multichain": "Multicadena", "global_add_money": "Agregar dinero", "global_buy_crypto": "Comprar criptomonedas", + "global_sign_in": "Iniciar sesión", + "google_account_not_signed_in": "Cuenta de Google no iniciada", + "google_play_services_not_available_desc": "Instala Google Play Services e inicia sesión con tu cuenta de Google.", + "google_play_services_not_available_title": "Google Play Services no está disponible", "hardware.backup_completed": "¡Copia de seguridad completada!", "hardware.bluetooth_need_turned_on_error": "El Bluetooth está apagado", "hardware.bluetooth_not_paired_error": "Bluetooth no emparejado", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Recomienda a un amigo", "id.refer_a_friend_desc": "Invita a tus amigos para ganar recompensas", "identical_name_asset_alert": "Hemos detectado activos con nombres similares en tu billetera. Procede con precaución.", + "import_backup_password_desc": "Introduce la contraseña de esta copia de seguridad.", + "import_hardware_phrases_warning": "No importes la frase de recuperación de tu hardware wallet. Conectar hardware wallet ↗ en su lugar", "import_phrase_or_private_key": "Importar frase o clave privada", "insufficient_fee_append_desc": "basado en la tarifa máxima estimada: {amount} {symbol}", "interact_with_contract": "Interactuar con (Para)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Conectando tu monedero de hardware para acceder a la cuenta Lightning", "ln.authorize_access_network_error": "La autenticación falló, verifica tu conexión a la red e intenta de nuevo", "ln.payment_received_label": "Pago recibido", + "log_out_confirmation_text": "¿Seguro que quieres cerrar sesión en ${email}?", + "logged_out_feedback": "Cierre de sesión exitoso", "login.forgot_passcode": "¿Olvidaste el código?", "login.forgot_password": "¿Olvidó su contraseña?", "login.welcome_message": "Bienvenido de nuevo", @@ -1983,6 +1999,11 @@ "nft.token_address": "Dirección de token", "nft.token_id": "ID de token", "no_account": "Sin cuenta", + "no_backup_found_google_desc": "Verifica tu inicio de sesión en Google Drive, asegúrate de usar la cuenta de Google correcta o revisa tu conexión de red", + "no_backup_found_icloud_desc": "Verifica tu inicio de sesión en iCloud, habilita iCloud y la sincronización del llavero, o comprueba tu conexión de red", + "no_backup_found_no_wallet": "No hay billeteras disponibles para hacer copia de seguridad", + "no_backup_found_no_wallet_desc": "Primero, crea una billetera", + "no_backups_found": "No se encontraron copias de seguridad", "no_external_wallet_message": "No hay carteras externas conectadas. Enlaza una cartera de terceros para ver aquí.", "no_private_key_account_message": "No hay cuentas con clave privada. Agrega una nueva cuenta para administrar tus activos.", "no_standard_wallet_desc": "Aún no hay una billetera estándar", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Recibirás actualizaciones de cuenta en tiempo real, alertas de seguridad y más información importante.", "notifications.test_message_title": "¡Las notificaciones push están listas!", "notifications.windows_notifications_permission_desc": "Recibe notificaciones de aplicaciones y otros remitentes", + "older_backups_description": "Estas copias de seguridad se crearon en versiones anteriores. Desde la {version}, las copias de seguridad de la billetera son manuales y los datos de la libreta de direcciones ahora forman parte de OneKey Cloud Sync en lugar de las copias de seguridad en la nube.", "onboarding.activate_device": "Activa tu dispositivo", "onboarding.activate_device_all_set": "¡Todo listo!", "onboarding.activate_device_by_restore": "Restaurar billetera", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Guardar imagen", "perps.share_position_title": "Compartir posición", "pick_your_device": "Elige tu dispositivo", + "preparing_backup_desc": "Un momento…", + "preparing_backup_title": "Preparando la copia de seguridad…", "prime.about_cloud_sync": "Acerca de la sincronización con la nube", "prime.about_cloud_sync_description": "Conoce qué datos se sincronizan y cómo protegemos tu privacidad.", "prime.about_cloud_sync_included_data_title": "Datos sincronizados", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Buscar símbolo o dirección de contrato", "send_token_selector.select_token": "Seleccionar Token", "sending_krc20_warning_text": "Enviar tokens KRC20 requiere dos confirmaciones en su hardware. No cancele la segunda confirmación, o el KAS enviado en la primera confirmación será difícil de recuperar.", + "set_new_backup_password": "Establecer nueva contraseña de copia de seguridad", + "set_new_backup_password_desc": "La nueva contraseña de copia de seguridad se usará para tu próximo registro de copia de seguridad. Los registros de copia de seguridad existentes y sus contraseñas permanecerán iguales.", "setting.floating_icon": "Icono flotante", "setting.floating_icon_always_display": "Mostrar siempre", "setting.floating_icon_always_display_description": "Cuando está habilitado, el icono de OneKey flota en el borde de la página web, ayudándote a verificar rápidamente la información de seguridad de las dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Estándar (Electrum): funciona con la mayoría de las billeteras y servicios", "signature_format_title": "Formato de firma", "signature_type_not_supported_on_model": "{deviceModel} no es compatible con el formato de firma {sigType}", + "signed_in_feedback": "Inicio de sesión correcto", "skip_firmware_check_dialog_desc": "¿Seguro que quieres omitir la verificación? Usar firmware actualizado te ofrece la mejor protección.", "skip_firmware_check_dialog_title": "¿Omitir la comprobación del firmware?", "skip_verify_text": "No tengo mi dispositivo conmigo", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "No cierres tu aplicación durante la actualización", "v4_migration.welcome_message": "¡OneKey 5.0 ya está aquí!", "v4_migration.welcome_message_desc": "Aquí te explicamos cómo migrar tus datos de manera segura y rápida. ¿Listo para empezar?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Verificar la contraseña de la copia de seguridad", + "verify_backup_password_desc": "Confirma tu contraseña de copia de seguridad en la nube para continuar. Si la has olvidado, puedes restablecerla usando \"¿Olvidaste tu contraseña?\"", "verify_message_address_form_description": "Compatible con: {networks}", + "view_older_backups": "Ver copias de seguridad anteriores", "wallet.approval_alert_title_summary": "Se detectaron {riskyNumber} aprobaciones de riesgo y {inactiveNumber} contratos inactivos", "wallet.approval_approval_details": "Detalles de la autorización", "wallet.approval_approved_token": "Token aprobado", diff --git a/packages/shared/src/locale/json/fr_FR.json b/packages/shared/src/locale/json/fr_FR.json index ce1462f0e7d4..636bb7462089 100644 --- a/packages/shared/src/locale/json/fr_FR.json +++ b/packages/shared/src/locale/json/fr_FR.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Définir le code d'accès", "auth.set_password": "Définir le mot de passe", "auth.with_biometric": "Authentification avec {biometric}", + "backing_up_desc": "Ne fermez pas cette fenêtre avant la fin de la sauvegarde.", + "backing_up_title": "Sauvegarde en cours…", "backup.address_book_labels": "Carnet d'adresses & étiquettes", "backup.all_devices": "Tous les appareils", "backup.backup_deleted": "Sauvegarde supprimée", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Vérifiez le mot de passe de l'application au moment de la sauvegarde pour importer des données.", "backup.verify_apple_account_and_icloud_drive_enabled": "Veuillez vérifier votre connexion à votre compte Apple et assurez-vous que iCloud Drive est activé et autorisé pour OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Veuillez vérifier votre connexion à votre compte Google et assurez-vous que Google Drive est activé et autorisé pour OneKey.", + "backup_restored": "Sauvegarde restaurée !", + "backup_success_toast_title": "C'est fait ! Votre sauvegarde est terminée", + "backup_write_to_cloud_failed": "Oups ! La sauvegarde a échoué cette fois. Veuillez réessayer", "balance_detail.button_acknowledge": "Reconnaître", "balance_detail.button_balance": "Détails du solde", "balance_detail.button_cancel": "Annuler", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Prendre {number} jours après avoir émis une demande de retrait", "earn.withdrawal_up_to_number_days": "Le retrait peut prendre jusqu'à {number} jours, puis vos actifs mis en jeu seront disponibles", "earn.withdrawn": "Retiré", + "earn_no_assets_deposited": "Aucun actif déposé pour le moment", "earn_reward_distribution_schedule": "Les récompenses seront distribuées sur votre portefeuille Arbitrum (même adresse qu'Ethereum) d'ici le 10 du mois prochain.", "edit_fee_custom_set_as_default_description": "Définir comme valeur par défaut pour toutes les transactions futures sur {network}", "energy_consumed": "Énergie consommée", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Transfert de données", "firmware_update.status_validating": "Validation", "for_reference_only": "Pour référence uniquement", + "forgot_password_no_question_mark": "Mot de passe oublié", "form.address_error_invalid": "Adresse invalide", "form.address_placeholder": "Adresse ou domaine", "form.amount_placeholder": "Entrez le montant", @@ -1101,7 +1108,7 @@ "global.allow": "Autoriser", "global.always": "Toujours", "global.an_error_occurred": "Une erreur s'est produite", - "global.an_error_occurred_desc": "Nous ne pouvons pas traiter votre demande. Veuillez actualiser la page dans quelques minutes.", + "global.an_error_occurred_desc": "Veuillez actualiser la page et réessayer dans quelques minutes.", "global.app_wallet": "Portefeuille d'application", "global.apply": "Postuler", "global.approval": "Approbation", @@ -1111,6 +1118,7 @@ "global.approve": "Approuver", "global.apr": "APR", "global.asset": "Actif", + "global.at_least_variable_characters": "Au moins {variable} caractères", "global.auto": "Auto", "global.available": "Disponible", "global.backed_up": "Sauvegardé", @@ -1657,6 +1665,10 @@ "global__multichain": "Multichaîne", "global_add_money": "Ajouter de l'argent", "global_buy_crypto": "Acheter des cryptos", + "global_sign_in": "Se connecter", + "google_account_not_signed_in": "Compte Google non connecté", + "google_play_services_not_available_desc": "Veuillez installer Google Play Services et vous connecter avec votre compte Google.", + "google_play_services_not_available_title": "Les services Google Play ne sont pas disponibles", "hardware.backup_completed": "Sauvegarde terminée !", "hardware.bluetooth_need_turned_on_error": "Le Bluetooth est éteint", "hardware.bluetooth_not_paired_error": "Bluetooth non appairé", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Parrainer un ami", "id.refer_a_friend_desc": "Invitez des amis pour gagner des récompenses", "identical_name_asset_alert": "Nous avons détecté des actifs avec des noms similaires dans votre portefeuille. Procédez avec prudence.", + "import_backup_password_desc": "Veuillez saisir le mot de passe pour cette sauvegarde.", + "import_hardware_phrases_warning": "N'importez pas la phrase de récupération de votre portefeuille matériel. Connecter un portefeuille matériel ↗ à la place", "import_phrase_or_private_key": "Importer une phrase ou une clé privée", "insufficient_fee_append_desc": "basé sur les frais max estimés : {amount} {symbol}", "interact_with_contract": "Interagir avec (À)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Connecter votre portefeuille matériel pour accéder au compte Lightning", "ln.authorize_access_network_error": "L'authentification a échoué, vérifiez votre connexion réseau et essayez à nouveau", "ln.payment_received_label": "Paiement reçu", + "log_out_confirmation_text": "Voulez-vous vraiment vous déconnecter de ${email} ?", + "logged_out_feedback": "Déconnexion réussie", "login.forgot_passcode": "Mot de passe oublié ?", "login.forgot_password": "Mot de passe oublié ?", "login.welcome_message": "Bienvenue à nouveau", @@ -1983,6 +1999,11 @@ "nft.token_address": "Adresse du jeton", "nft.token_id": "ID de jeton", "no_account": "Aucun compte", + "no_backup_found_google_desc": "Veuillez vérifier votre connexion à Google Drive, assurez-vous d'utiliser le bon compte Google ou vérifiez votre connexion réseau", + "no_backup_found_icloud_desc": "Veuillez vérifier votre connexion iCloud, activer la synchronisation iCloud et Trousseau, ou vérifier votre connexion réseau", + "no_backup_found_no_wallet": "Aucun portefeuille disponible à sauvegarder", + "no_backup_found_no_wallet_desc": "Veuillez d'abord créer un portefeuille", + "no_backups_found": "Aucune sauvegarde trouvée", "no_external_wallet_message": "Aucun portefeuille externe n'est connecté. Liez un portefeuille tiers pour l'afficher ici.", "no_private_key_account_message": "Aucun compte à clé privée. Ajoutez un nouveau compte pour gérer vos actifs.", "no_standard_wallet_desc": "Pas encore de portefeuille standard", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Vous recevrez des mises à jour de compte en temps réel, des alertes de sécurité et d'autres informations importantes.", "notifications.test_message_title": "Les notifications push sont prêtes !", "notifications.windows_notifications_permission_desc": "Recevez des notifications d'applications et d'autres expéditeurs", + "older_backups_description": "Ces sauvegardes ont été créées dans des versions antérieures. Depuis la version {version}, les sauvegardes de portefeuille sont manuelles, et les données du carnet d'adresses font désormais partie de OneKey Cloud Sync au lieu des sauvegardes cloud.", "onboarding.activate_device": "Activez votre appareil", "onboarding.activate_device_all_set": "Tout est prêt !", "onboarding.activate_device_by_restore": "Restaurer le portefeuille", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Enregistrer l'image", "perps.share_position_title": "Partager la position", "pick_your_device": "Choisissez votre appareil", + "preparing_backup_desc": "Un instant…", + "preparing_backup_title": "Préparation de la sauvegarde…", "prime.about_cloud_sync": "À propos de la synchronisation cloud", "prime.about_cloud_sync_description": "Découvrez quelles données sont synchronisées et de quelle manière votre vie privée est protégée.", "prime.about_cloud_sync_included_data_title": "Données synchronisées", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Rechercher un symbole ou une adresse de contrat", "send_token_selector.select_token": "Sélectionner le jeton", "sending_krc20_warning_text": "L'envoi de jetons KRC20 nécessite deux confirmations sur votre matériel. Ne pas annuler la deuxième confirmation, sinon le KAS envoyé lors de la première confirmation sera difficile à récupérer.", + "set_new_backup_password": "Définir un nouveau mot de passe de sauvegarde", + "set_new_backup_password_desc": "Le nouveau mot de passe de sauvegarde est utilisé pour votre prochain enregistrement de sauvegarde. Les enregistrements de sauvegarde existants et leurs mots de passe resteront inchangés.", "setting.floating_icon": "Icône flottante", "setting.floating_icon_always_display": "Toujours afficher", "setting.floating_icon_always_display_description": "Lorsqu'il est activé, l'icône OneKey flotte sur le bord de la page web, vous aidant à vérifier rapidement les informations de sécurité des dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Standard (Electrum) — Fonctionne avec la plupart des portefeuilles et services", "signature_format_title": "Format de signature", "signature_type_not_supported_on_model": "{sigType} n'est pas pris en charge sur l'appareil : {deviceModel}", + "signed_in_feedback": "Connexion réussie", "skip_firmware_check_dialog_desc": "Êtes-vous sûr de vouloir ignorer la vérification ? L'utilisation d'un micrologiciel à jour vous offre la meilleure protection.", "skip_firmware_check_dialog_title": "Ignorer la vérification du firmware ?", "skip_verify_text": "Je n'ai pas mon appareil avec moi", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "N'arrêtez pas votre application pendant la mise à jour", "v4_migration.welcome_message": "OneKey 5.0 est là !", "v4_migration.welcome_message_desc": "Voici comment migrer vos données de manière sécurisée et rapide. Prêt à commencer ?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Vérifier le mot de passe de sauvegarde", + "verify_backup_password_desc": "Veuillez confirmer votre mot de passe de sauvegarde cloud pour continuer. Si vous l'avez oublié, vous pouvez le réinitialiser en utilisant Mot de passe oublié.", "verify_message_address_form_description": "Prise en charge : {networks}", + "view_older_backups": "Afficher les sauvegardes plus anciennes", "wallet.approval_alert_title_summary": "{riskyNumber} approbations à risque et {inactiveNumber} contrats inactifs détectés", "wallet.approval_approval_details": "Détails d'autorisation", "wallet.approval_approved_token": "Jeton approuvé", diff --git a/packages/shared/src/locale/json/hi_IN.json b/packages/shared/src/locale/json/hi_IN.json index 00c99fe4be99..d2b1fc261581 100644 --- a/packages/shared/src/locale/json/hi_IN.json +++ b/packages/shared/src/locale/json/hi_IN.json @@ -158,6 +158,8 @@ "auth.set_passcode": "पासकोड सेट करें", "auth.set_password": "पासवर्ड सेट करें", "auth.with_biometric": "{biometric} के साथ प्रमाणीकरण", + "backing_up_desc": "जब तक बैकअप पूरा न हो जाए, इस विंडो को बंद न करें।", + "backing_up_title": "बैकअप लिया जा रहा है…", "backup.address_book_labels": "पता पुस्तिका और लेबल", "backup.all_devices": "सभी उपकरण", "backup.backup_deleted": "बैकअप हटा दिया गया", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "डेटा आयात करने के लिए बैकअप के समय ऐप पासवर्ड सत्यापित करें।", "backup.verify_apple_account_and_icloud_drive_enabled": "कृपया अपने एप्पल खाते की लॉगिन की पुष्टि करें और सुनिश्चित करें कि iCloud Drive सक्षम है और OneKey के लिए अधिकृत है।", "backup.verify_google_account_and_google_drive_enabled": "कृपया अपने Google खाते की लॉगिन की पुष्टि करें और सुनिश्चित करें कि Google Drive सक्षम है और OneKey के लिए अधिकृत है।", + "backup_restored": "बैकअप बहाल हो गया!", + "backup_success_toast_title": "सब तैयार! आपका बैकअप पूरा हो गया है", + "backup_write_to_cloud_failed": "उफ़! इस बार बैकअप असफल रहा। कृपया फिर से प्रयास करें", "balance_detail.button_acknowledge": "स्वीकार करें", "balance_detail.button_balance": "बैलेंस विवरण", "balance_detail.button_cancel": "रद्द करें", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "निकासी अनुरोध जारी करने के {number} दिन बाद लें", "earn.withdrawal_up_to_number_days": "निकासी {number} दिनों तक ले सकती है, और फिर आपके दांव किए गए संपत्तियां उपलब्ध होंगी", "earn.withdrawn": "वापस लिया गया", + "earn_no_assets_deposited": "अभी तक कोई संपत्ति जमा नहीं की गई", "earn_reward_distribution_schedule": "पुरस्कार अगले महीने की 10 तारीख तक आपके आर्बिट्रम वॉलेट (इथेरियम के समान पता) में वितरित कर दिए जाएंगे।", "edit_fee_custom_set_as_default_description": "{network} पर सभी भविष्य के लेनदेन के लिए डिफ़ॉल्ट के रूप में सेट करें", "energy_consumed": "खपत की गई ऊर्जा", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "डेटा स्थानांतरित करना", "firmware_update.status_validating": "सत्यापन कर रहा है", "for_reference_only": "केवल संदर्भ हेतु", + "forgot_password_no_question_mark": "पासवर्ड भूल गए", "form.address_error_invalid": "अमान्य पता", "form.address_placeholder": "पता या डोमेन", "form.amount_placeholder": "राशि दर्ज करें", @@ -1101,7 +1108,7 @@ "global.allow": "अनुमति देना", "global.always": "हमेशा", "global.an_error_occurred": "एक त्रुटि हुई", - "global.an_error_occurred_desc": "हम आपके अनुरोध को पूरा करने में असमर्थ हैं। कृपया कुछ मिनटों में पृष्ठ को ताज़ा करें।", + "global.an_error_occurred_desc": "कृपया रीफ़्रेश करें और कुछ मिनट बाद पुनः प्रयास करें.", "global.app_wallet": "ऐप वॉलेट", "global.apply": "लागू करें", "global.approval": "मंज़ूरी", @@ -1111,6 +1118,7 @@ "global.approve": "स्वीकार करें", "global.apr": "APR", "global.asset": "संपत्ति", + "global.at_least_variable_characters": "कम से कम {variable} वर्ण", "global.auto": "ऑटो", "global.available": "उपलब्ध", "global.backed_up": "बैकअप किया गया", @@ -1657,6 +1665,10 @@ "global__multichain": "मल्टीचेन", "global_add_money": "पैसे जोड़ें", "global_buy_crypto": "क्रिप्टो खरीदें", + "global_sign_in": "साइन इन", + "google_account_not_signed_in": "Google खाता साइन इन नहीं है", + "google_play_services_not_available_desc": "कृपया Google Play Services इंस्टॉल करें और अपने Google खाते से साइन इन करें।", + "google_play_services_not_available_title": "Google Play Services उपलब्ध नहीं है", "hardware.backup_completed": "बैकअप पूरा हुआ!", "hardware.bluetooth_need_turned_on_error": "ब्लूटूथ बंद है", "hardware.bluetooth_not_paired_error": "ब्लूटूथ जोड़ा नहीं गया है", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "एक मित्र को संदर्भित करें", "id.refer_a_friend_desc": "दोस्तों को आमंत्रित करें और पुरस्कार प्राप्त करें", "identical_name_asset_alert": "हमने आपके वॉलेट में समान नाम वाले एसेट्स का पता लगाया है। सावधानी से आगे बढ़ें।", + "import_backup_password_desc": "कृपया इस बैकअप के लिए पासवर्ड दर्ज करें।", + "import_hardware_phrases_warning": "अपने हार्डवेयर वॉलेट का रिकवरी फ़्रेज आयात न करें। हार्डवेयर वॉलेट कनेक्ट करें ↗", "import_phrase_or_private_key": "वाक्यांश या निजी कुंजी आयात करें", "insufficient_fee_append_desc": "अधिकतम अनुमानित शुल्क के आधार पर: {amount} {symbol}", "interact_with_contract": "(से) संवाद करें", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "अपने हार्डवेयर वॉलेट को लाइटनिंग खाता एक्सेस करने के लिए कनेक्ट करें", "ln.authorize_access_network_error": "प्रमाणीकरण विफल रहा, कृपया अपने नेटवर्क कनेक्शन की जांच करें और पुनः प्रयास करें", "ln.payment_received_label": "भुगतान प्राप्त", + "log_out_confirmation_text": "क्या आप वाकई ${email} से लॉग आउट करना चाहते हैं?", + "logged_out_feedback": "सफलतापूर्वक लॉग आउट किया गया", "login.forgot_passcode": "पासकोड भूल गए?", "login.forgot_password": "पासवर्ड भूल गए?", "login.welcome_message": "फिर से स्वागत है", @@ -1983,6 +1999,11 @@ "nft.token_address": "टोकन पता", "nft.token_id": "टोकन आईडी", "no_account": "कोई खाता नहीं", + "no_backup_found_google_desc": "कृपया अपना Google Drive साइन-इन सत्यापित करें, सुनिश्चित करें कि आप सही Google खाते का उपयोग कर रहे हैं, या अपना नेटवर्क कनेक्शन जांचें", + "no_backup_found_icloud_desc": "कृपया अपने iCloud साइन-इन की पुष्टि करें, iCloud और Keychain सिंक सक्षम करें, या अपने नेटवर्क कनेक्शन की जाँच करें", + "no_backup_found_no_wallet": "बैकअप के लिए कोई वॉलेट उपलब्ध नहीं", + "no_backup_found_no_wallet_desc": "कृपया पहले एक वॉलेट बनाएँ", + "no_backups_found": "कोई बैकअप नहीं मिला", "no_external_wallet_message": "कोई बाहरी वॉलेट कनेक्टेड नहीं हैं। यहां देखने के लिए एक तीसरे पक्ष का वॉलेट लिंक करें।", "no_private_key_account_message": "कोई निजी कुंजी खाते नहीं हैं। अपने संपत्ति का प्रबंधन करने के लिए एक नया खाता जोड़ें।", "no_standard_wallet_desc": "अभी तक कोई मानक वॉलेट नहीं है", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "आपको वास्तविक समय में खाता अपडेट, सुरक्षा अलर्ट और अन्य महत्वपूर्ण जानकारी प्राप्त होगी।", "notifications.test_message_title": "पुश नोटिफिकेशन तैयार हैं!", "notifications.windows_notifications_permission_desc": "ऐप्स और अन्य प्रेषकों से अधिसूचनाएं प्राप्त करें", + "older_backups_description": "ये बैकअप पहले के संस्करणों में बनाए गए थे। संस्करण {version} से, वॉलेट बैकअप मैन्युअल हैं, और एड्रेस बुक डेटा अब क्लाउड बैकअप के बजाय OneKey Cloud Sync का हिस्सा है।", "onboarding.activate_device": "अपने उपकरण को सक्रिय करें", "onboarding.activate_device_all_set": "सब तैयार!", "onboarding.activate_device_by_restore": "वॉलेट को पुनर्स्थापित करें", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "छवि सहेजें", "perps.share_position_title": "स्थिति साझा करें", "pick_your_device": "अपना डिवाइस चुनें", + "preparing_backup_desc": "बस एक पल…", + "preparing_backup_title": "बैकअप तैयार किया जा रहा है…", "prime.about_cloud_sync": "क्लाउड सिंक के बारे में", "prime.about_cloud_sync_description": "जानें कि कौन सा डेटा सिंक होता है और आपकी प्राइवेसी कैसे सुरक्षित रहती है।", "prime.about_cloud_sync_included_data_title": "सिंक होने वाला डेटा", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "संकेत या संविदा पते की खोज करें", "send_token_selector.select_token": "टोकन चुनें", "sending_krc20_warning_text": "KRC20 टोकन भेजने के लिए आपके हार्डवेयर पर दो पुष्टियों की आवश्यकता होती है। दूसरी पुष्टि को रद्द न करें, अन्यथा पहली पुष्टि में भेजे गए KAS को पुनः प्राप्त करना कठिन होगा।", + "set_new_backup_password": "नया बैकअप पासवर्ड सेट करें", + "set_new_backup_password_desc": "नई बैकअप पासवर्ड आपके अगले बैकअप रिकॉर्ड के लिए उपयोग किया जाएगा। मौजूदा बैकअप रिकॉर्ड और उनके पासवर्ड पहले जैसे ही रहेंगे।", "setting.floating_icon": "फ्लोटिंग आइकन", "setting.floating_icon_always_display": "हमेशा प्रदर्शित करें", "setting.floating_icon_always_display_description": "सक्रिय होने पर, OneKey आइकन वेबपेज के किनारे पर तैरता है, जिससे आप dApps की सुरक्षा जानकारी को जल्दी से जांच सकते हैं।", @@ -3080,6 +3106,7 @@ "signature_format_standard": "मानक (इलेक्ट्रम) — अधिकांश वॉलेट और सेवाओं के साथ काम करता है", "signature_format_title": "हस्ताक्षर प्रारूप", "signature_type_not_supported_on_model": "डिवाइस {deviceModel} पर {sigType} समर्थित नहीं है", + "signed_in_feedback": "सफलतापूर्वक साइन इन हो गया", "skip_firmware_check_dialog_desc": "क्या आप वाकई जाँच छोड़ना चाहते हैं? नवीनतम firmware का उपयोग सर्वोत्तम सुरक्षा देता है।", "skip_firmware_check_dialog_title": "फ़र्मवेयर की जाँच छोड़ें?", "skip_verify_text": "मेरे पास अपना उपकरण नहीं है", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "अपडेट के दौरान अपने ऐप को बंद न करें", "v4_migration.welcome_message": "OneKey 5.0 यहाँ है!", "v4_migration.welcome_message_desc": "यहाँ देखिए कि आप अपना डाटा कैसे सुरक्षित और तेजी से स्थानांतरित कर सकते हैं। शुरू करने के लिए तैयार हैं?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "बैकअप पासवर्ड सत्यापित करें", + "verify_backup_password_desc": "कृपया आगे बढ़ने के लिए अपना क्लाउड बैकअप पासवर्ड पुष्टि करें। यदि आप इसे भूल गए हैं, तो आप \"Forgot password\" का उपयोग करके इसे रीसेट कर सकते हैं।", "verify_message_address_form_description": "समर्थित: {networks}", + "view_older_backups": "पुराने बैकअप देखें", "wallet.approval_alert_title_summary": "{riskyNumber} जोखिम भरे अनुमोदन और {inactiveNumber} निष्क्रिय अनुबंधों का पता चला।", "wallet.approval_approval_details": "प्राधिकरण का विवरण", "wallet.approval_approved_token": "स्वीकृत टोकन", diff --git a/packages/shared/src/locale/json/id.json b/packages/shared/src/locale/json/id.json index bdc18387293c..764f3b6e893c 100644 --- a/packages/shared/src/locale/json/id.json +++ b/packages/shared/src/locale/json/id.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Atur kode sandi", "auth.set_password": "Atur kata sandi", "auth.with_biometric": "Otentikasi dengan {biometric}", + "backing_up_desc": "Jangan tutup jendela ini sampai pencadangan selesai.", + "backing_up_title": "Mencadangkan…", "backup.address_book_labels": "Buku alamat & label", "backup.all_devices": "Semua perangkat", "backup.backup_deleted": "Cadangan dihapus", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Verifikasi kata sandi Aplikasi pada saat pencadangan untuk Impor data.", "backup.verify_apple_account_and_icloud_drive_enabled": "Silakan verifikasi login akun Apple Anda dan pastikan iCloud Drive diaktifkan dan diotorisasi untuk OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Silakan verifikasi login akun Google Anda dan pastikan Google Drive diaktifkan dan diotorisasi untuk OneKey.", + "backup_restored": "Cadangan dipulihkan!", + "backup_success_toast_title": "Semua siap! Cadangan Anda sudah selesai", + "backup_write_to_cloud_failed": "Ups! Pencadangan gagal kali ini. Silakan coba lagi", "balance_detail.button_acknowledge": "Mengakui", "balance_detail.button_balance": "Rincian Saldo", "balance_detail.button_cancel": "Batalkan", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Ambil {number} hari setelah mengajukan permintaan penarikan", "earn.withdrawal_up_to_number_days": "Penarikan dapat memakan waktu hingga {number} hari, dan setelah itu aset yang Anda staking akan tersedia", "earn.withdrawn": "Ditarik", + "earn_no_assets_deposited": "Belum ada aset yang disetorkan", "earn_reward_distribution_schedule": "Hadiah akan didistribusikan ke dompet Arbitrum Anda (alamat yang sama dengan Ethereum) paling lambat tanggal 10 bulan depan.", "edit_fee_custom_set_as_default_description": "Tetapkan sebagai default untuk semua transaksi di masa mendatang pada {network}", "energy_consumed": "Energi yang dikonsumsi", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Mentransfer data", "firmware_update.status_validating": "Memvalidasi", "for_reference_only": "Hanya untuk referensi", + "forgot_password_no_question_mark": "Lupa kata sandi", "form.address_error_invalid": "Alamat tidak valid", "form.address_placeholder": "Alamat atau domain", "form.amount_placeholder": "Masukkan jumlah", @@ -1101,7 +1108,7 @@ "global.allow": "Mengizinkan", "global.always": "Selalu", "global.an_error_occurred": "Terjadi kesalahan", - "global.an_error_occurred_desc": "Kami tidak dapat menyelesaikan permintaan Anda. Silakan segarkan halaman dalam beberapa menit.", + "global.an_error_occurred_desc": "Harap segarkan dan coba lagi dalam beberapa menit.", "global.app_wallet": "Dompet aplikasi", "global.apply": "Terapkan", "global.approval": "Persetujuan", @@ -1111,6 +1118,7 @@ "global.approve": "Setujui", "global.apr": "APR", "global.asset": "Aset", + "global.at_least_variable_characters": "Setidaknya {variable} karakter", "global.auto": "Mobil", "global.available": "Tersedia", "global.backed_up": "Di-backup", @@ -1657,6 +1665,10 @@ "global__multichain": "Multi-rantai", "global_add_money": "Tambah uang", "global_buy_crypto": "Beli kripto", + "global_sign_in": "Masuk", + "google_account_not_signed_in": "Akun Google belum masuk", + "google_play_services_not_available_desc": "Silakan instal Google Play Services dan masuk dengan akun Google Anda.", + "google_play_services_not_available_title": "Layanan Google Play tidak tersedia", "hardware.backup_completed": "Pencadangan selesai!", "hardware.bluetooth_need_turned_on_error": "Bluetooth dimatikan", "hardware.bluetooth_not_paired_error": "Bluetooth tidak dipasangkan", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Ajak Teman", "id.refer_a_friend_desc": "Undang teman untuk mendapatkan hadiah", "identical_name_asset_alert": "Kami mendeteksi aset dengan nama yang mirip di dompet Anda. Harap lanjutkan dengan hati-hati.", + "import_backup_password_desc": "Silakan masukkan kata sandi untuk cadangan ini.", + "import_hardware_phrases_warning": "Jangan impor frasa pemulihan dompet perangkat keras Anda. Sambungkan Dompet Perangkat Keras ↗ sebagai gantinya", "import_phrase_or_private_key": "Impor frasa atau kunci privat", "insufficient_fee_append_desc": "berdasarkan perkiraan biaya maksimum: {amount} {symbol}", "interact_with_contract": "Berinteraksi dengan (Kepada)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Menghubungkan dompet hardware Anda untuk mengakses akun Lightning", "ln.authorize_access_network_error": "Otentikasi gagal, periksa koneksi jaringan Anda dan coba lagi", "ln.payment_received_label": "Pembayaran diterima", + "log_out_confirmation_text": "Apakah Anda yakin ingin keluar dari ${email}?", + "logged_out_feedback": "Berhasil keluar", "login.forgot_passcode": "Lupa kode sandi?", "login.forgot_password": "Lupa kata sandi?", "login.welcome_message": "Selamat kembali", @@ -1983,6 +1999,11 @@ "nft.token_address": "Alamat Token", "nft.token_id": "ID Token", "no_account": "Tidak ada akun", + "no_backup_found_google_desc": "Harap verifikasi masuk Google Drive Anda, pastikan Anda menggunakan akun Google yang benar, atau periksa koneksi jaringan Anda", + "no_backup_found_icloud_desc": "Harap verifikasi masuk iCloud Anda, aktifkan sinkronisasi iCloud dan Keychain, atau periksa koneksi jaringan Anda", + "no_backup_found_no_wallet": "Tidak ada dompet yang tersedia untuk dicadangkan", + "no_backup_found_no_wallet_desc": "Harap buat dompet terlebih dahulu", + "no_backups_found": "Tidak ada cadangan ditemukan", "no_external_wallet_message": "Tidak ada dompet eksternal yang terhubung. Hubungkan dompet pihak ketiga untuk dilihat di sini.", "no_private_key_account_message": "Tidak ada akun kunci pribadi. Tambahkan akun baru untuk mengelola aset Anda.", "no_standard_wallet_desc": "Belum ada dompet standar", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Anda akan menerima pembaruan akun secara real-time, peringatan keamanan, dan informasi penting lainnya.", "notifications.test_message_title": "Pemberitahuan push sudah siap!", "notifications.windows_notifications_permission_desc": "Dapatkan notifikasi dari aplikasi dan pengirim lainnya", + "older_backups_description": "Cadangan ini dibuat di versi sebelumnya. Sejak {version}, cadangan dompet dilakukan secara manual, dan data buku alamat sekarang menjadi bagian dari OneKey Cloud Sync, bukan cadangan cloud.", "onboarding.activate_device": "Aktifkan perangkat Anda", "onboarding.activate_device_all_set": "Semua siap!", "onboarding.activate_device_by_restore": "Pulihkan dompet", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Simpan Gambar", "perps.share_position_title": "Bagikan Posisi", "pick_your_device": "Pilih perangkat Anda", + "preparing_backup_desc": "Tunggu sebentar…", + "preparing_backup_title": "Menyiapkan cadangan…", "prime.about_cloud_sync": "Tentang Cloud Sync", "prime.about_cloud_sync_description": "Pelajari data apa saja yang disinkronkan dan bagaimana privasi Anda dilindungi.", "prime.about_cloud_sync_included_data_title": "Data yang Disertakan", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Cari simbol atau alamat kontrak", "send_token_selector.select_token": "Pilih Token", "sending_krc20_warning_text": "Mengirim token KRC20 memerlukan dua konfirmasi pada perangkat keras Anda. Jangan batalkan konfirmasi kedua, atau KAS yang dikirim pada konfirmasi pertama akan sulit untuk dipulihkan.", + "set_new_backup_password": "Atur sandi cadangan baru", + "set_new_backup_password_desc": "Kata sandi cadangan baru akan digunakan untuk catatan cadangan Anda berikutnya. Catatan cadangan yang sudah ada beserta kata sandinya akan tetap sama.", "setting.floating_icon": "Ikon mengambang", "setting.floating_icon_always_display": "Selalu tampilkan", "setting.floating_icon_always_display_description": "Ketika diaktifkan, ikon OneKey melayang di tepi halaman web, membantu Anda dengan cepat memeriksa informasi keamanan dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Standar (Electrum) — Berfungsi dengan sebagian besar dompet dan layanan", "signature_format_title": "Format tanda tangan", "signature_type_not_supported_on_model": "{deviceModel} tidak mendukung format tanda tangan {sigType}", + "signed_in_feedback": "Berhasil masuk", "skip_firmware_check_dialog_desc": "Yakin ingin melewati pemeriksaan? Menggunakan firmware terbaru memberi Anda perlindungan terbaik.", "skip_firmware_check_dialog_title": "Lewati pemeriksaan firmware?", "skip_verify_text": "Saya tidak membawa perangkat saya", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Jangan tutup aplikasi Anda selama pembaruan", "v4_migration.welcome_message": "OneKey 5.0 sudah hadir!", "v4_migration.welcome_message_desc": "Berikut cara untuk memigrasikan data Anda dengan cepat dan aman. Siap untuk memulai?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Verifikasi kata sandi cadangan", + "verify_backup_password_desc": "Harap konfirmasi kata sandi cadangan cloud Anda untuk melanjutkan. Jika Anda lupa, Anda dapat mengatur ulangnya melalui Lupa kata sandi.", "verify_message_address_form_description": "Mendukung: {networks}", + "view_older_backups": "Lihat cadangan yang lebih lama", "wallet.approval_alert_title_summary": "Terdeteksi {riskyNumber} persetujuan berisiko dan {inactiveNumber} kontrak tidak aktif", "wallet.approval_approval_details": "Detail otorisasi", "wallet.approval_approved_token": "Token yang Disetujui", diff --git a/packages/shared/src/locale/json/it_IT.json b/packages/shared/src/locale/json/it_IT.json index 847e3a27f8e9..1664f52a60d0 100644 --- a/packages/shared/src/locale/json/it_IT.json +++ b/packages/shared/src/locale/json/it_IT.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Imposta codice di accesso", "auth.set_password": "Imposta password", "auth.with_biometric": "Autenticazione con {biometric}", + "backing_up_desc": "Non chiudere questa finestra finché il backup non è completo.", + "backing_up_title": "Backup in corso…", "backup.address_book_labels": "Rubrica & etichette", "backup.all_devices": "Tutti i dispositivi", "backup.backup_deleted": "Backup eliminato", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Verificare la password dell'app al momento del backup su Importa dati.", "backup.verify_apple_account_and_icloud_drive_enabled": "Verifica il tuo accesso all'account Apple e assicurati che iCloud Drive sia abilitato e autorizzato per OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Verifica il tuo accesso all'account Google e assicurati che Google Drive sia abilitato e autorizzato per OneKey.", + "backup_restored": "Backup ripristinato!", + "backup_success_toast_title": "Tutto pronto! Il backup è completo", + "backup_write_to_cloud_failed": "Ops! Il backup non è riuscito questa volta. Riprova", "balance_detail.button_acknowledge": "Riconosci", "balance_detail.button_balance": "Dettagli del Saldo", "balance_detail.button_cancel": "Annulla", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Occorrono {number} giorni dopo aver effettuato una richiesta di prelievo", "earn.withdrawal_up_to_number_days": "Il prelievo può richiedere fino a {number} giorni, dopodiché i tuoi asset investiti saranno disponibili", "earn.withdrawn": "Ritirato", + "earn_no_assets_deposited": "Nessun bene depositato ancora", "earn_reward_distribution_schedule": "Le ricompense verranno distribuite sul tuo portafoglio Arbitrum (stesso indirizzo di Ethereum) entro il 10 del mese prossimo.", "edit_fee_custom_set_as_default_description": "Imposta come predefinito per tutte le future transazioni su {network}", "energy_consumed": "Energia consumata", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Trasferimento dati", "firmware_update.status_validating": "Convalida", "for_reference_only": "Solo per riferimento", + "forgot_password_no_question_mark": "Password dimenticata", "form.address_error_invalid": "Indirizzo non valido", "form.address_placeholder": "Indirizzo o dominio", "form.amount_placeholder": "Inserisci importo", @@ -1101,7 +1108,7 @@ "global.allow": "Permettere", "global.always": "Sempre", "global.an_error_occurred": "Si è verificato un errore", - "global.an_error_occurred_desc": "Non siamo in grado di completare la tua richiesta. Si prega di aggiornare la pagina tra qualche minuto.", + "global.an_error_occurred_desc": "Aggiorna la pagina e riprova tra qualche minuto.", "global.app_wallet": "Portafoglio App", "global.apply": "Applica", "global.approval": "Approvazione", @@ -1111,6 +1118,7 @@ "global.approve": "Approva", "global.apr": "APR", "global.asset": "Asset", + "global.at_least_variable_characters": "Almeno {variable} caratteri", "global.auto": "Auto", "global.available": "Disponibile", "global.backed_up": "Salvato", @@ -1657,6 +1665,10 @@ "global__multichain": "Multichain", "global_add_money": "Aggiungi denaro", "global_buy_crypto": "Acquista criptovalute", + "global_sign_in": "Accedi", + "google_account_not_signed_in": "Account Google non connesso", + "google_play_services_not_available_desc": "Installa Google Play Services e accedi con il tuo account Google.", + "google_play_services_not_available_title": "I servizi di Google Play non sono disponibili", "hardware.backup_completed": "Backup completato!", "hardware.bluetooth_need_turned_on_error": "Il Bluetooth è spento", "hardware.bluetooth_not_paired_error": "Bluetooth non accoppiato", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Segnala un amico", "id.refer_a_friend_desc": "Invita amici per guadagnare ricompense", "identical_name_asset_alert": "Abbiamo rilevato asset con nomi simili nel tuo portafoglio. Procedi con cautela.", + "import_backup_password_desc": "Inserisci la password per questo backup.", + "import_hardware_phrases_warning": "Non importare la frase di recupero del tuo hardware wallet. Collega hardware wallet ↗ invece", "import_phrase_or_private_key": "Importa frase o chiave privata", "insufficient_fee_append_desc": "basato sulla tariffa massima stimata: {amount} {symbol}", "interact_with_contract": "Interagisci con (A)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Collegare il tuo portafoglio hardware per accedere all'account Lightning", "ln.authorize_access_network_error": "Autenticazione fallita, controlla la tua connessione di rete e prova di nuovo", "ln.payment_received_label": "Pagamento ricevuto", + "log_out_confirmation_text": "Sei sicuro di voler uscire da ${email}?", + "logged_out_feedback": "Disconnessione riuscita", "login.forgot_passcode": "Hai dimenticato il codice?", "login.forgot_password": "Hai dimenticato la password?", "login.welcome_message": "Bentornato", @@ -1983,6 +1999,11 @@ "nft.token_address": "Indirizzo Token", "nft.token_id": "ID del token", "no_account": "Nessun account", + "no_backup_found_google_desc": "Verifica l’accesso a Google Drive, assicurati di usare l’account Google corretto oppure controlla la connessione di rete", + "no_backup_found_icloud_desc": "Verifica l’accesso a iCloud, abilita iCloud e la sincronizzazione del Portachiavi oppure controlla la connessione di rete", + "no_backup_found_no_wallet": "Nessun portafoglio disponibile per il backup", + "no_backup_found_no_wallet_desc": "Crea prima un wallet", + "no_backups_found": "Nessun backup trovato", "no_external_wallet_message": "Nessun portafoglio esterno è collegato. Collega un portafoglio di terze parti per visualizzarlo qui.", "no_private_key_account_message": "Non ci sono account con chiave privata. Aggiungi un nuovo account per gestire i tuoi asset.", "no_standard_wallet_desc": "Nessun portafoglio standard ancora", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Riceverai aggiornamenti in tempo reale sul tuo account, avvisi di sicurezza e altre informazioni importanti.", "notifications.test_message_title": "Le notifiche push sono pronte!", "notifications.windows_notifications_permission_desc": "Ricevi notifiche da app e altri mittenti", + "older_backups_description": "Questi backup sono stati creati in versioni precedenti. Dalla {version}, i backup del wallet sono manuali e i dati della rubrica ora fanno parte di OneKey Cloud Sync invece dei backup sul cloud.", "onboarding.activate_device": "Attiva il tuo dispositivo", "onboarding.activate_device_all_set": "Tutto pronto!", "onboarding.activate_device_by_restore": "Ripristina portafoglio", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Salva immagine", "perps.share_position_title": "Condividi posizione", "pick_your_device": "Scegli il tuo dispositivo", + "preparing_backup_desc": "Un momento…", + "preparing_backup_title": "Preparazione del backup…", "prime.about_cloud_sync": "Info sulla sincronizzazione cloud", "prime.about_cloud_sync_description": "Scopri quali dati vengono sincronizzati e come proteggiamo la tua privacy.", "prime.about_cloud_sync_included_data_title": "Dati inclusi", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Cerca simbolo o indirizzo del contratto", "send_token_selector.select_token": "Seleziona Token", "sending_krc20_warning_text": "L'invio di token KRC20 richiede due conferme sul tuo hardware. Non annullare la seconda conferma, altrimenti il KAS inviato nella prima conferma sarà difficile da recuperare.", + "set_new_backup_password": "Imposta una nuova password di backup", + "set_new_backup_password_desc": "La nuova password di backup verrà utilizzata per il tuo prossimo record di backup. I record di backup esistenti e le loro password resteranno invariati.", "setting.floating_icon": "Icona fluttuante", "setting.floating_icon_always_display": "Mostra sempre", "setting.floating_icon_always_display_description": "Quando abilitato, l'icona OneKey fluttua sul bordo della pagina web, aiutandoti a controllare rapidamente le informazioni di sicurezza dei dApp.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Standard (Electrum) — Funziona con la maggior parte dei wallet e dei servizi", "signature_format_title": "Formato della firma", "signature_type_not_supported_on_model": "{deviceModel} non supporta il formato di firma {sigType}", + "signed_in_feedback": "Accesso effettuato con successo", "skip_firmware_check_dialog_desc": "Sei sicuro di voler saltare il controllo? Usare firmware aggiornato offre la migliore protezione.", "skip_firmware_check_dialog_title": "Saltare il controllo del firmware?", "skip_verify_text": "Non ho il mio dispositivo con me", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Non chiudere la tua app durante l'aggiornamento", "v4_migration.welcome_message": "OneKey 5.0 è qui!", "v4_migration.welcome_message_desc": "Ecco come migrare i tuoi dati in modo sicuro e veloce. Pronto per iniziare?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Verifica la password di backup", + "verify_backup_password_desc": "Conferma la password del backup cloud per continuare. Se l’hai dimenticata, puoi reimpostarla usando \"Password dimenticata\".", "verify_message_address_form_description": "Supporta: {networks}", + "view_older_backups": "Visualizza backup meno recenti", "wallet.approval_alert_title_summary": "Rilevate {riskyNumber} approvazioni rischiose e {inactiveNumber} contratti inattivi", "wallet.approval_approval_details": "Dettagli dell'autorizzazione", "wallet.approval_approved_token": "Token approvato", diff --git a/packages/shared/src/locale/json/ja_JP.json b/packages/shared/src/locale/json/ja_JP.json index 8d1a25a76e2d..6a12cf293e79 100644 --- a/packages/shared/src/locale/json/ja_JP.json +++ b/packages/shared/src/locale/json/ja_JP.json @@ -158,6 +158,8 @@ "auth.set_passcode": "パスコードを設定する", "auth.set_password": "パスワードを設定する", "auth.with_biometric": "{biometric}による認証", + "backing_up_desc": "バックアップが完了するまで、このウィンドウを閉じないでください。", + "backing_up_title": "バックアップ中…", "backup.address_book_labels": "アドレス帳&ラベル", "backup.all_devices": "すべてのデバイス", "backup.backup_deleted": "バックアップが削除されました", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "データをインポートするには、バックアップ時にアプリのパスワードを確認します。", "backup.verify_apple_account_and_icloud_drive_enabled": "Appleアカウントのログインを確認し、iCloud Driveが有効でOneKeyに対して認証されていることを確認してください。", "backup.verify_google_account_and_google_drive_enabled": "Googleアカウントのログインを確認し、Google Driveが有効でOneKeyに対して認証されていることを確認してください。", + "backup_restored": "バックアップを復元しました!", + "backup_success_toast_title": "準備完了!バックアップが完了しました", + "backup_write_to_cloud_failed": "おっと!今回はバックアップに失敗しました。もう一度お試しください", "balance_detail.button_acknowledge": "認知する", "balance_detail.button_balance": "残高詳細", "balance_detail.button_cancel": "キャンセル", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "出金リクエストを出してから{number}日後に受け取ります", "earn.withdrawal_up_to_number_days": "引き出しは最大{number}日かかり、その後、ステークされた資産が利用可能になります", "earn.withdrawn": "撤回", + "earn_no_assets_deposited": "まだ資産は預けられていません", "earn_reward_distribution_schedule": "報酬は来月10日までにArbitrumウォレット(Ethereumと同じアドレス)に配布されます。", "edit_fee_custom_set_as_default_description": "{network}のすべての将来のトランザクションのデフォルトとして設定する", "energy_consumed": "消費エネルギー", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "データの転送", "firmware_update.status_validating": "検証中", "for_reference_only": "参考のためだけ", + "forgot_password_no_question_mark": "パスワードをお忘れですか", "form.address_error_invalid": "無効なアドレス", "form.address_placeholder": "アドレスまたはドメイン", "form.amount_placeholder": "金額を入力してください", @@ -1101,7 +1108,7 @@ "global.allow": "許可する", "global.always": "常に", "global.an_error_occurred": "エラーが発生しました", - "global.an_error_occurred_desc": "あなたのリクエストを完了することができませんでした。数分後にページを更新してください。", + "global.an_error_occurred_desc": "更新して数分後にもう一度お試しください。", "global.app_wallet": "アプリウォレット", "global.apply": "申し込む", "global.approval": "承認", @@ -1111,6 +1118,7 @@ "global.approve": "承認", "global.apr": "APR", "global.asset": "資産", + "global.at_least_variable_characters": "少なくとも{variable}文字", "global.auto": "自動", "global.available": "利用可能", "global.backed_up": "バックアップ済み", @@ -1657,6 +1665,10 @@ "global__multichain": "マルチチェーン", "global_add_money": "入金する", "global_buy_crypto": "暗号資産を購入", + "global_sign_in": "サインイン", + "google_account_not_signed_in": "Google アカウントに未サインイン", + "google_play_services_not_available_desc": "Google Play開発者サービスをインストールし、Googleアカウントでサインインしてください。", + "google_play_services_not_available_title": "Google Play 開発者サービスは利用できません", "hardware.backup_completed": "バックアップが完了しました!", "hardware.bluetooth_need_turned_on_error": "Bluetoothはオフです", "hardware.bluetooth_not_paired_error": "Bluetoothがペアリングされていません", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "友達を紹介する", "id.refer_a_friend_desc": "友達を招待して報酬を獲得しましょう", "identical_name_asset_alert": "ウォレット内で似た名前の資産を検出しました。慎重に進めてください。", + "import_backup_password_desc": "このバックアップのパスワードを入力してください。", + "import_hardware_phrases_warning": "ハードウェアウォレットのリカバリーフレーズをインポートしないでください。 ハードウェアウォレットを接続 ↗ を使用してください", "import_phrase_or_private_key": "フレーズまたは秘密鍵をインポート", "insufficient_fee_append_desc": "最大推定手数料に基づく: {amount} {symbol}", "interact_with_contract": "(に)交流する", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Lightningアカウントにアクセスするためのハードウェアウォレットの接続", "ln.authorize_access_network_error": "認証に失敗しました、ネットワーク接続を確認して再試行してください", "ln.payment_received_label": "お支払い頂いた", + "log_out_confirmation_text": "${email} からログアウトしてもよろしいですか?", + "logged_out_feedback": "ログアウトしました", "login.forgot_passcode": "パスコードを忘れましたか?", "login.forgot_password": "パスワードを忘れましたか?", "login.welcome_message": "おかえりなさい", @@ -1983,6 +1999,11 @@ "nft.token_address": "トークンアドレス", "nft.token_id": "トークンID", "no_account": "アカウントなし", + "no_backup_found_google_desc": "Google Drive へのサインインを確認し、正しい Google アカウントを使用していることを確かめるか、ネットワーク接続を確認してください", + "no_backup_found_icloud_desc": "iCloudへのサインインを確認し、iCloudとキーチェーンの同期を有効にするか、ネットワーク接続を確認してください", + "no_backup_found_no_wallet": "バックアップできるウォレットがありません", + "no_backup_found_no_wallet_desc": "まずウォレットを作成してください", + "no_backups_found": "バックアップが見つかりません", "no_external_wallet_message": "外部のウォレットが接続されていません。ここで表示するためには、サードパーティのウォレットをリンクしてください。", "no_private_key_account_message": "プライベートキーアカウントがありません。新しいアカウントを追加して資産を管理してください。", "no_standard_wallet_desc": "標準ウォレットはまだありません", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "リアルタイムのアカウント更新、セキュリティアラート、その他重要な情報を受け取ることができます。", "notifications.test_message_title": "プッシュ通知の準備ができました!", "notifications.windows_notifications_permission_desc": "アプリや他の送信者からの通知を受け取る", + "older_backups_description": "これらのバックアップは以前のバージョンで作成されたものです。{version}以降、ウォレットのバックアップは手動になり、アドレス帳データはクラウドバックアップではなく OneKey Cloud Sync の一部になりました。", "onboarding.activate_device": "あなたのデバイスを有効化する", "onboarding.activate_device_all_set": "全てセット完了!", "onboarding.activate_device_by_restore": "ウォレットを復元", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "画像を保存", "perps.share_position_title": "ポジションを共有", "pick_your_device": "デバイスを選択", + "preparing_backup_desc": "少々お待ちください…", + "preparing_backup_title": "バックアップを準備中…", "prime.about_cloud_sync": "クラウド同期について", "prime.about_cloud_sync_description": "同期されるデータと、プライバシーの保護方法についてご確認ください。", "prime.about_cloud_sync_included_data_title": "同期データ", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "シンボルまたは契約アドレスを検索", "send_token_selector.select_token": "トークンを選択", "sending_krc20_warning_text": "KRC20トークンを送信するには、ハードウェア上で2回の確認が必要です。2回目の確認をキャンセルしないでください。最初の確認で送信されたKASは回収が困難になります。", + "set_new_backup_password": "新しいバックアップパスワードを設定", + "set_new_backup_password_desc": "新しいバックアップ用パスワードは、次回のバックアップ記録に使用されます。既存のバックアップ記録とそのパスワードは変更されません。", "setting.floating_icon": "フローティングアイコン", "setting.floating_icon_always_display": "常に表示", "setting.floating_icon_always_display_description": "有効にすると、OneKeyアイコンがウェブページの端に浮かび、dAppsのセキュリティ情報をすばやく確認するのに役立ちます。", @@ -3080,6 +3106,7 @@ "signature_format_standard": "標準(Electrum)— ほとんどのウォレットやサービスで利用可能", "signature_format_title": "署名形式", "signature_type_not_supported_on_model": "{deviceModel} では {sigType} 署名形式はサポートされていません", + "signed_in_feedback": "正常にサインインしました", "skip_firmware_check_dialog_desc": "この確認をスキップしてもよろしいですか?最新のファームウェアを使うと、最高の保護が得られます。", "skip_firmware_check_dialog_title": "ファームウェアのチェックをスキップしますか?", "skip_verify_text": "私のデバイスが手元にありません", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "アップデート中はアプリを閉じないでください", "v4_migration.welcome_message": "OneKey 5.0が登場しました!", "v4_migration.welcome_message_desc": "ここでは、データを安全かつ迅速に移行する方法を説明します。始める準備はできていますか?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "バックアップ用パスワードを確認", + "verify_backup_password_desc": "続行するにはクラウドバックアップのパスワードを確認してください。忘れてしまった場合は、「パスワードをお忘れですか」からリセットできます。", "verify_message_address_form_description": "対応ネットワーク: {networks}", + "view_older_backups": "過去のバックアップを表示", "wallet.approval_alert_title_summary": "{riskyNumber} 件の危険な承認と {inactiveNumber} 件の非アクティブな契約が検出されました", "wallet.approval_approval_details": "承認詳細", "wallet.approval_approved_token": "承認済みトークン", diff --git a/packages/shared/src/locale/json/ko_KR.json b/packages/shared/src/locale/json/ko_KR.json index 3489cf418645..96944f0b4156 100644 --- a/packages/shared/src/locale/json/ko_KR.json +++ b/packages/shared/src/locale/json/ko_KR.json @@ -158,6 +158,8 @@ "auth.set_passcode": "암호 설정", "auth.set_password": "비밀번호 설정", "auth.with_biometric": "{biometric}을 이용한 인증", + "backing_up_desc": "백업이 완료될 때까지 이 창을 닫지 마세요.", + "backing_up_title": "백업 중…", "backup.address_book_labels": "주소록 & 라벨", "backup.all_devices": "모든 장치", "backup.backup_deleted": "백업 삭제됨", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "가져오기 데이터 백업 시 앱 비밀번호를 확인하세요.", "backup.verify_apple_account_and_icloud_drive_enabled": "Apple 계정 로그인을 확인하고 iCloud Drive가 활성화되어 있으며 OneKey에 대한 권한이 부여되었는지 확인하십시오.", "backup.verify_google_account_and_google_drive_enabled": "Google 계정 로그인을 확인하고 Google Drive가 OneKey에 대해 활성화되고 인증되었는지 확인하십시오.", + "backup_restored": "백업이 복원되었습니다!", + "backup_success_toast_title": "준비 완료! 백업이 완료되었습니다", + "backup_write_to_cloud_failed": "이런! 이번에는 백업에 실패했어요. 다시 시도해 주세요", "balance_detail.button_acknowledge": "인정하다", "balance_detail.button_balance": "잔액 상세", "balance_detail.button_cancel": "취소", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "출금 요청을 한 후 {number} 일이 소요됩니다", "earn.withdrawal_up_to_number_days": "출금은 최대 {number}일까지 걸릴 수 있으며, 그 후에는 스테이크된 자산을 사용할 수 있습니다", "earn.withdrawn": "출금", + "earn_no_assets_deposited": "아직 예치된 자산이 없습니다", "earn_reward_distribution_schedule": "보상은 다음 달 10일까지 귀하의 Arbitrum 지갑(이더리움과 동일한 주소)으로 분배됩니다.", "edit_fee_custom_set_as_default_description": "{network}에서 모든 향후 거래에 대해 기본값으로 설정", "energy_consumed": "소비된 에너지", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "데이터 전송", "firmware_update.status_validating": "유효성 검사 중", "for_reference_only": "참고용으로만 사용하세요", + "forgot_password_no_question_mark": "비밀번호를 잊으셨나요", "form.address_error_invalid": "잘못된 주소", "form.address_placeholder": "주소 또는 도메인", "form.amount_placeholder": "금액 입력", @@ -1101,7 +1108,7 @@ "global.allow": "허용하다", "global.always": "언제나", "global.an_error_occurred": "오류가 발생했습니다", - "global.an_error_occurred_desc": "요청을 완료할 수 없습니다. 몇 분 후에 페이지를 새로 고침해 주세요.", + "global.an_error_occurred_desc": "새로고침하고 몇 분 후에 다시 시도해 보세요.", "global.app_wallet": "앱 지갑", "global.apply": "적용", "global.approval": "승인", @@ -1111,6 +1118,7 @@ "global.approve": "승인", "global.apr": "APR", "global.asset": "자산", + "global.at_least_variable_characters": "최소 {variable}자", "global.auto": "자동", "global.available": "사용 가능", "global.backed_up": "백업됨", @@ -1657,6 +1665,10 @@ "global__multichain": "다중체인", "global_add_money": "금액 추가", "global_buy_crypto": "암호화폐 구매", + "global_sign_in": "로그인", + "google_account_not_signed_in": "Google 계정에 로그인되지 않음", + "google_play_services_not_available_desc": "Google Play 서비스 설치 후 Google 계정으로 로그인하세요.", + "google_play_services_not_available_title": "Google Play 서비스을(를) 사용할 수 없습니다", "hardware.backup_completed": "백업 완료!", "hardware.bluetooth_need_turned_on_error": "블루투스가 꺼져 있습니다", "hardware.bluetooth_not_paired_error": "블루투스가 페어링되지 않았습니다", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "친구 추천", "id.refer_a_friend_desc": "친구를 초대하여 보상을 받으세요", "identical_name_asset_alert": "지갑에서 이름이 비슷한 자산을 감지했습니다. 주의해서 진행하세요.", + "import_backup_password_desc": "이 백업의 비밀번호를 입력하세요.", + "import_hardware_phrases_warning": "하드웨어 지갑의 복구 구문을 가져오지 마세요. 하드웨어 지갑 연결 ↗을(를) 대신 사용하세요", "import_phrase_or_private_key": "구문 또는 개인 키 가져오기", "insufficient_fee_append_desc": "최대 예상 수수료 기준: {amount} {symbol}", "interact_with_contract": "(~와) 상호 작용하기", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "라이트닝 계정에 하드웨어 지갑을 연결하기", "ln.authorize_access_network_error": "인증에 실패했습니다, 네트워크 연결을 확인하고 다시 시도해보세요", "ln.payment_received_label": "지불 수신", + "log_out_confirmation_text": "${email}에서 로그아웃하시겠습니까?", + "logged_out_feedback": "성공적으로 로그아웃되었습니다", "login.forgot_passcode": "암호를 잊으셨나요?", "login.forgot_password": "비밀번호를 잊으셨나요?", "login.welcome_message": "다시 만나서 반가워요", @@ -1983,6 +1999,11 @@ "nft.token_address": "토큰 주소", "nft.token_id": "토큰 ID", "no_account": "계정 없음", + "no_backup_found_google_desc": "Google Drive 로그인 상태를 확인하고, 올바른 Google 계정을 사용 중인지 확인하거나, 네트워크 연결을 점검하세요", + "no_backup_found_icloud_desc": "iCloud 로그인 확인, iCloud 및 키체인 동기화 활성화, 또는 네트워크 연결 확인을 해주세요", + "no_backup_found_no_wallet": "백업할 지갑이 없습니다", + "no_backup_found_no_wallet_desc": "먼저 월렛을 만들어 주세요", + "no_backups_found": "백업을 찾을 수 없습니다", "no_external_wallet_message": "외부 지갑이 연결되어 있지 않습니다. 여기에서 보려면 제3자 지갑을 연결하세요.", "no_private_key_account_message": "개인키 계정이 없습니다. 자산을 관리하기 위해 새 계정을 추가하세요.", "no_standard_wallet_desc": "아직 표준 지갑이 없습니다", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "실시간 계정 업데이트, 보안 경고 및 기타 중요한 정보를 받게 됩니다.", "notifications.test_message_title": "푸시 알림이 준비되었습니다!", "notifications.windows_notifications_permission_desc": "앱 및 기타 발신자로부터 알림 받기", + "older_backups_description": "이 백업은 이전 버전에서 생성되었습니다. {version}부터는 지갑 백업이 수동으로 진행되며, 주소록 데이터는 클라우드 백업 대신 OneKey Cloud Sync의 일부가 되었습니다.", "onboarding.activate_device": "장치를 활성화하세요", "onboarding.activate_device_all_set": "모두 준비되었습니다!", "onboarding.activate_device_by_restore": "지갑 복원", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "이미지 저장", "perps.share_position_title": "포지션 공유", "pick_your_device": "기기를 선택하세요", + "preparing_backup_desc": "잠시만요…", + "preparing_backup_title": "백업을 준비하는 중…", "prime.about_cloud_sync": "클라우드 동기화에 대하여", "prime.about_cloud_sync_description": "동기화되는 데이터와 개인정보 보호 방법을 알아보세요.", "prime.about_cloud_sync_included_data_title": "동기화 데이터", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "티커 또는 컨트랙트 주소 검색", "send_token_selector.select_token": "토큰 선택", "sending_krc20_warning_text": "KRC20 토큰을 전송하려면 하드웨어에서 두 번의 확인이 필요합니다. 두 번째 확인을 취소하지 마십시오. 첫 번째 확인에서 전송된 KAS는 복구하기 어려울 수 있습니다.", + "set_new_backup_password": "새 백업 비밀번호 설정", + "set_new_backup_password_desc": "새 백업 비밀번호는 다음 백업 기록에 사용됩니다. 기존 백업 기록과 해당 비밀번호는 그대로 유지됩니다.", "setting.floating_icon": "플로팅 아이콘", "setting.floating_icon_always_display": "항상 표시", "setting.floating_icon_always_display_description": "활성화되면 OneKey 아이콘이 웹페이지 가장자리에 떠서 dApps의 보안 정보를 빠르게 확인할 수 있도록 도와줍니다.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "표준(일렉트럼) — 대부분의 지갑 및 서비스와 호환됨", "signature_format_title": "서명 형식", "signature_type_not_supported_on_model": "{deviceModel} 기기에서는 {sigType} 서명 형식이 지원되지 않습니다.", + "signed_in_feedback": "성공적으로 로그인되었습니다", "skip_firmware_check_dialog_desc": "검사를 건너뛰시겠어요? 최신 펌웨어를 사용하면 가장 강력하게 보호됩니다.", "skip_firmware_check_dialog_title": "펌웨어 확인을 건너뛸까요?", "skip_verify_text": "제 기기가 제게 없습니다", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "업데이트 중에 앱을 닫지 마세요", "v4_migration.welcome_message": "OneKey 5.0이 출시되었습니다!", "v4_migration.welcome_message_desc": "여기에는 데이터를 안전하고 빠르게 이전하는 방법이 있습니다. 시작할 준비가 되셨나요?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "백업 비밀번호 확인", + "verify_backup_password_desc": "계속하려면 클라우드 백업 비밀번호를 확인하세요. 비밀번호를 잊으셨다면 ‘비밀번호 찾기’를 사용해 재설정할 수 있습니다.", "verify_message_address_form_description": "지원: {networks}", + "view_older_backups": "이전 백업 보기", "wallet.approval_alert_title_summary": "{riskyNumber}개의 위험한 승인과 {inactiveNumber}개의 비활성 계약이 감지되었습니다", "wallet.approval_approval_details": "승인 세부정보", "wallet.approval_approved_token": "승인된 토큰", diff --git a/packages/shared/src/locale/json/pt.json b/packages/shared/src/locale/json/pt.json index 0302c910e539..d1120eea682c 100644 --- a/packages/shared/src/locale/json/pt.json +++ b/packages/shared/src/locale/json/pt.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Definir código de acesso", "auth.set_password": "Definir senha", "auth.with_biometric": "Autenticação com {biometric}", + "backing_up_desc": "Não feche esta janela até que o backup esteja completo.", + "backing_up_title": "Fazendo backup…", "backup.address_book_labels": "Agenda de endereços & etiquetas", "backup.all_devices": "Todos os dispositivos", "backup.backup_deleted": "Backup excluído", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Verifique a senha do aplicativo no momento do backup para importar dados.", "backup.verify_apple_account_and_icloud_drive_enabled": "Por favor, verifique o login da sua conta Apple e certifique-se de que o iCloud Drive está ativado e autorizado para o OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Por favor, verifique o login da sua conta Google e certifique-se de que o Google Drive está ativado e autorizado para o OneKey.", + "backup_restored": "Backup restaurado!", + "backup_success_toast_title": "Tudo pronto! Seu backup está completo", + "backup_write_to_cloud_failed": "Ops! O backup falhou desta vez. Por favor, tente novamente", "balance_detail.button_acknowledge": "Reconhecer", "balance_detail.button_balance": "Detalhes do Saldo", "balance_detail.button_cancel": "Cancelar", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Leve {number} dias após fazer uma solicitação de retirada", "earn.withdrawal_up_to_number_days": "O saque pode levar até {number} dias, e então seus ativos apostados estarão disponíveis", "earn.withdrawn": "Retirado", + "earn_no_assets_deposited": "Nenhum ativo foi depositado ainda.", "earn_reward_distribution_schedule": "As recompensas serão distribuídas para sua carteira Arbitrum (mesmo endereço do Ethereum) até o dia 10 do mês que vem.", "edit_fee_custom_set_as_default_description": "Definir como padrão para todas as transações futuras na {network}", "energy_consumed": "Energia consumida", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Transferindo dados", "firmware_update.status_validating": "Validando", "for_reference_only": "Para referência apenas", + "forgot_password_no_question_mark": "Esqueceu a senha", "form.address_error_invalid": "Endereço inválido", "form.address_placeholder": "Endereço ou domínio", "form.amount_placeholder": "Insira o valor", @@ -1101,7 +1108,7 @@ "global.allow": "Permitir", "global.always": "Sempre", "global.an_error_occurred": "Ocorreu um erro", - "global.an_error_occurred_desc": "Não conseguimos concluir a sua solicitação. Atualize a página em alguns minutos.", + "global.an_error_occurred_desc": "Por favor, atualize a página e tente novamente em alguns minutos.", "global.app_wallet": "Carteira de aplicativo", "global.apply": "Aplicar", "global.approval": "Aprovação", @@ -1111,6 +1118,7 @@ "global.approve": "Aprovar", "global.apr": "APR", "global.asset": "Ativo", + "global.at_least_variable_characters": "Pelo menos {variable} caracteres", "global.auto": "Auto", "global.available": "Disponível", "global.backed_up": "Com backup", @@ -1657,6 +1665,10 @@ "global__multichain": "Multicadeia", "global_add_money": "Adicionar dinheiro", "global_buy_crypto": "Comprar cripto", + "global_sign_in": "Iniciar sessão", + "google_account_not_signed_in": "Conta do Google não conectada", + "google_play_services_not_available_desc": "Instale o Google Play Services e faça login com sua conta do Google.", + "google_play_services_not_available_title": "Os Serviços do Google Play não estão disponíveis", "hardware.backup_completed": "Backup concluído!", "hardware.bluetooth_need_turned_on_error": "O Bluetooth está desligado", "hardware.bluetooth_not_paired_error": "Bluetooth não pareado", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Indique um Amigo", "id.refer_a_friend_desc": "Convide amigos para ganhar recompensas", "identical_name_asset_alert": "Detectamos ativos com nomes similares na sua carteira. Proceda com cautela.", + "import_backup_password_desc": "Por favor, insira a senha para este backup.", + "import_hardware_phrases_warning": "Não importe a frase de recuperação da sua carteira de hardware. Conectar Carteira de Hardware ↗ em vez disso", "import_phrase_or_private_key": "Importar frase ou chave privada", "insufficient_fee_append_desc": "com base na taxa máxima estimada: {amount} {symbol}", "interact_with_contract": "Interagir com (Para)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Conectando sua carteira de hardware para acessar a conta Lightning", "ln.authorize_access_network_error": "A autenticação falhou, verifique sua conexão de rede e tente novamente", "ln.payment_received_label": "Pagamento recebido", + "log_out_confirmation_text": "Tem certeza de que deseja sair de ${email}?", + "logged_out_feedback": "Sessão encerrada com sucesso", "login.forgot_passcode": "Esqueceu o código de acesso?", "login.forgot_password": "Esqueceu a senha?", "login.welcome_message": "Bem-vindo de volta", @@ -1983,6 +1999,11 @@ "nft.token_address": "Endereço do Token", "nft.token_id": "ID do Token", "no_account": "Sem conta", + "no_backup_found_google_desc": "Verifique o seu início de sessão no Google Drive, certifique-se de que está a usar a conta Google correta ou verifique a sua ligação de rede", + "no_backup_found_icloud_desc": "Verifique o seu início de sessão no iCloud, ative a sincronização do iCloud e do Porta-chaves ou verifique a sua ligação de rede", + "no_backup_found_no_wallet": "Nenhuma carteira disponível para fazer backup", + "no_backup_found_no_wallet_desc": "Por favor, crie uma carteira primeiro", + "no_backups_found": "Nenhum backup encontrado", "no_external_wallet_message": "Nenhuma carteira externa está conectada. Conecte uma carteira de terceiros para visualizar aqui.", "no_private_key_account_message": "Não há contas com chave privada. Adicione uma nova conta para gerenciar seus ativos.", "no_standard_wallet_desc": "Ainda sem carteira padrão", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Você receberá atualizações em tempo real da conta, alertas de segurança e outras informações importantes.", "notifications.test_message_title": "As notificações push estão prontas!", "notifications.windows_notifications_permission_desc": "Receba notificações de aplicativos e outros remetentes", + "older_backups_description": "Esses backups foram criados em versões anteriores. Desde a versão {version}, os backups da carteira são manuais e os dados do catálogo de endereços agora fazem parte do OneKey Cloud Sync em vez dos backups na nuvem.", "onboarding.activate_device": "Ative o seu dispositivo", "onboarding.activate_device_all_set": "Tudo pronto!", "onboarding.activate_device_by_restore": "Restaurar carteira", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Salvar Imagem", "perps.share_position_title": "Compartilhar Posição", "pick_your_device": "Escolha o seu dispositivo", + "preparing_backup_desc": "Só um momento…", + "preparing_backup_title": "Preparando backup…", "prime.about_cloud_sync": "Sobre a Sincronização na Nuvem", "prime.about_cloud_sync_description": "Saiba quais dados são sincronizados e como a sua privacidade é protegida.", "prime.about_cloud_sync_included_data_title": "Dados Sincronizados", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Pesquise símbolo ou endereço de contrato", "send_token_selector.select_token": "Selecionar Token", "sending_krc20_warning_text": "Enviar tokens KRC20 requer duas confirmações no seu hardware. Não cancele a segunda confirmação, ou o KAS enviado na primeira confirmação será difícil de recuperar.", + "set_new_backup_password": "Definir nova senha de backup", + "set_new_backup_password_desc": "A nova senha de backup é usada para o seu próximo registro de backup. Os registros de backup existentes e suas senhas permanecerão os mesmos.", "setting.floating_icon": "Ícone flutuante", "setting.floating_icon_always_display": "Sempre exibir", "setting.floating_icon_always_display_description": "Quando ativado, o ícone OneKey flutua na borda da página da web, ajudando você a verificar rapidamente as informações de segurança dos dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Padrão (Electrum) — Funciona com a maioria das carteiras e serviços", "signature_format_title": "Formato de assinatura", "signature_type_not_supported_on_model": "{sigType} não é compatível com o dispositivo: {deviceModel}", + "signed_in_feedback": "Login efetuado com sucesso", "skip_firmware_check_dialog_desc": "Tem certeza de que deseja ignorar a verificação? Usar o firmware atualizado oferece a melhor proteção.", "skip_firmware_check_dialog_title": "Ignorar verificação de firmware?", "skip_verify_text": "Não tenho meu dispositivo comigo", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Não feche seu aplicativo durante a atualização", "v4_migration.welcome_message": "OneKey 5.0 está aqui!", "v4_migration.welcome_message_desc": "Aqui está como migrar seus dados de forma segura e rápida. Pronto para começar?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Verificar senha de backup", + "verify_backup_password_desc": "Confirme a sua palavra-passe de cópia de segurança na nuvem para continuar. Se a esqueceu, pode redefini-la usando Esqueci-me da palavra-passe.", "verify_message_address_form_description": "Suporta: {networks}", + "view_older_backups": "Ver backups mais antigos", "wallet.approval_alert_title_summary": "Detetadas {riskyNumber} aprovações de risco e {inactiveNumber} contratos inativos", "wallet.approval_approval_details": "Detalhes da autorização", "wallet.approval_approved_token": "Token aprovado", diff --git a/packages/shared/src/locale/json/pt_BR.json b/packages/shared/src/locale/json/pt_BR.json index 4772be6434ab..6166a90178d6 100644 --- a/packages/shared/src/locale/json/pt_BR.json +++ b/packages/shared/src/locale/json/pt_BR.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Definir código de acesso", "auth.set_password": "Definir senha", "auth.with_biometric": "Autenticação com {biometric}", + "backing_up_desc": "Não feche esta janela até que o backup seja concluído.", + "backing_up_title": "Fazendo backup…", "backup.address_book_labels": "Agenda de endereços & etiquetas", "backup.all_devices": "Todos os dispositivos", "backup.backup_deleted": "Backup excluído", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Verifique a senha do aplicativo no momento do backup para importar dados.", "backup.verify_apple_account_and_icloud_drive_enabled": "Por favor, verifique o login da sua conta Apple e certifique-se de que o iCloud Drive está habilitado e autorizado para o OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Por favor, verifique o login da sua conta Google e certifique-se de que o Google Drive está habilitado e autorizado para o OneKey.", + "backup_restored": "Backup restaurado!", + "backup_success_toast_title": "Tudo pronto! Seu backup está completo", + "backup_write_to_cloud_failed": "Ops! O backup falhou desta vez. Por favor, tente novamente", "balance_detail.button_acknowledge": "Reconhecer", "balance_detail.button_balance": "Detalhes do Saldo", "balance_detail.button_cancel": "Cancelar", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Leve {number} dias após fazer uma solicitação de retirada", "earn.withdrawal_up_to_number_days": "O saque pode levar até {number} dias, e então seus ativos apostados estarão disponíveis", "earn.withdrawn": "Retirado", + "earn_no_assets_deposited": "Nenhum ativo foi depositado ainda.", "earn_reward_distribution_schedule": "As recompensas serão distribuídas para sua carteira Arbitrum (mesmo endereço do Ethereum) até o dia 10 do mês que vem.", "edit_fee_custom_set_as_default_description": "Definir como padrão para todas as transações futuras na {network}", "energy_consumed": "Energia consumida", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Transferindo dados", "firmware_update.status_validating": "Validando", "for_reference_only": "Para referência apenas", + "forgot_password_no_question_mark": "Esqueceu a senha", "form.address_error_invalid": "Endereço inválido", "form.address_placeholder": "Endereço ou domínio", "form.amount_placeholder": "Insira o valor", @@ -1101,7 +1108,7 @@ "global.allow": "Permitir", "global.always": "Sempre", "global.an_error_occurred": "Ocorreu um erro", - "global.an_error_occurred_desc": "Não conseguimos concluir sua solicitação. Atualize a página em alguns minutos.", + "global.an_error_occurred_desc": "Por favor, atualize a página e tente novamente em alguns minutos.", "global.app_wallet": "Carteira de aplicativo", "global.apply": "Aplicar", "global.approval": "Aprovação", @@ -1111,6 +1118,7 @@ "global.approve": "Aprovar", "global.apr": "APR", "global.asset": "Ativo", + "global.at_least_variable_characters": "Pelo menos {variable} caracteres", "global.auto": "Automático", "global.available": "Disponível", "global.backed_up": "Backup realizado", @@ -1657,6 +1665,10 @@ "global__multichain": "Multicadeia", "global_add_money": "Adicionar dinheiro", "global_buy_crypto": "Comprar cripto", + "global_sign_in": "Entrar", + "google_account_not_signed_in": "Conta do Google não conectada", + "google_play_services_not_available_desc": "Instale o Google Play Services e faça login com sua conta do Google.", + "google_play_services_not_available_title": "O Google Play Services não está disponível", "hardware.backup_completed": "Backup concluído!", "hardware.bluetooth_need_turned_on_error": "O Bluetooth está desligado", "hardware.bluetooth_not_paired_error": "Bluetooth não pareado", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Indique um Amigo", "id.refer_a_friend_desc": "Convide amigos para ganhar recompensas", "identical_name_asset_alert": "Detectamos ativos com nomes similares em sua carteira. Prossiga com cautela.", + "import_backup_password_desc": "Por favor, insira a senha para este backup.", + "import_hardware_phrases_warning": "Não importe a frase de recuperação da sua carteira de hardware. Conectar Carteira de Hardware ↗ em vez disso", "import_phrase_or_private_key": "Importar frase ou chave privada", "insufficient_fee_append_desc": "com base na taxa máxima estimada: {amount} {symbol}", "interact_with_contract": "Interagir com (Para)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Conectando sua carteira de hardware para acessar a conta Lightning", "ln.authorize_access_network_error": "A autenticação falhou, verifique sua conexão de rede e tente novamente", "ln.payment_received_label": "Pagamento recebido", + "log_out_confirmation_text": "Tem certeza de que deseja sair de ${email}?", + "logged_out_feedback": "Logout realizado com sucesso", "login.forgot_passcode": "Esqueceu a senha?", "login.forgot_password": "Esqueceu a senha?", "login.welcome_message": "Bem-vindo de volta", @@ -1983,6 +1999,11 @@ "nft.token_address": "Endereço do Token", "nft.token_id": "ID do Token", "no_account": "Sem conta", + "no_backup_found_google_desc": "Verifique seu login no Google Drive, certifique-se de que está usando a conta do Google correta ou verifique sua conexão de rede", + "no_backup_found_icloud_desc": "Verifique seu login no iCloud, ative a sincronização do iCloud e do Keychain ou verifique sua conexão de rede", + "no_backup_found_no_wallet": "Nenhuma carteira disponível para fazer backup", + "no_backup_found_no_wallet_desc": "Por favor, crie uma carteira primeiro", + "no_backups_found": "Nenhum backup encontrado", "no_external_wallet_message": "Nenhuma carteira externa está conectada. Conecte uma carteira de terceiros para visualizar aqui.", "no_private_key_account_message": "Não há contas com chave privada. Adicione uma nova conta para gerenciar seus ativos.", "no_standard_wallet_desc": "Nenhuma carteira padrão ainda", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Você receberá atualizações de conta em tempo real, alertas de segurança e outras informações importantes.", "notifications.test_message_title": "Notificações push estão prontas!", "notifications.windows_notifications_permission_desc": "Receba notificações de aplicativos e outros remetentes", + "older_backups_description": "Esses backups foram criados em versões anteriores. Desde a versão {version}, os backups de carteira são manuais, e os dados do catálogo de endereços agora fazem parte do OneKey Cloud Sync em vez dos backups na nuvem.", "onboarding.activate_device": "Ative o seu dispositivo", "onboarding.activate_device_all_set": "Tudo pronto!", "onboarding.activate_device_by_restore": "Restaurar carteira", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Salvar Imagem", "perps.share_position_title": "Compartilhar Posição", "pick_your_device": "Escolha seu dispositivo", + "preparing_backup_desc": "Só um momento…", + "preparing_backup_title": "Preparando backup…", "prime.about_cloud_sync": "Sobre a Sincronização na Nuvem", "prime.about_cloud_sync_description": "Saiba quais dados são sincronizados e como sua privacidade é protegida.", "prime.about_cloud_sync_included_data_title": "Dados Sincronizados", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Pesquise símbolo ou endereço de contrato", "send_token_selector.select_token": "Selecionar Token", "sending_krc20_warning_text": "Enviar tokens KRC20 requer duas confirmações no seu hardware. Não cancele a segunda confirmação, ou o KAS enviado na primeira confirmação será difícil de recuperar.", + "set_new_backup_password": "Definir nova senha de backup", + "set_new_backup_password_desc": "A nova senha de backup é usada para o próximo registro de backup. Os registros de backup existentes e suas senhas permanecerão os mesmos.", "setting.floating_icon": "Ícone flutuante", "setting.floating_icon_always_display": "Sempre exibir", "setting.floating_icon_always_display_description": "Quando ativado, o ícone OneKey flutua na borda da página da web, ajudando você a verificar rapidamente as informações de segurança dos dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Padrão (Electrum) — Funciona com a maioria das carteiras e serviços", "signature_format_title": "Formato de assinatura", "signature_type_not_supported_on_model": "{sigType} não é compatível com o dispositivo: {deviceModel}", + "signed_in_feedback": "Login realizado com sucesso", "skip_firmware_check_dialog_desc": "Tem certeza de que deseja pular a verificação? Usar o firmware atualizado oferece a melhor proteção.", "skip_firmware_check_dialog_title": "Pular verificação de firmware?", "skip_verify_text": "Eu não tenho meu dispositivo comigo", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Não feche seu aplicativo durante a atualização", "v4_migration.welcome_message": "OneKey 5.0 está aqui!", "v4_migration.welcome_message_desc": "Aqui está como migrar seus dados de forma segura e rápida. Pronto para começar?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Verificar senha de backup", + "verify_backup_password_desc": "Confirme sua senha de backup na nuvem para continuar. Se você a esqueceu, pode redefini-la usando Esqueci a senha.", "verify_message_address_form_description": "Suporta: {networks}", + "view_older_backups": "Ver backups mais antigos", "wallet.approval_alert_title_summary": "{riskyNumber} aprovações de risco e {inactiveNumber} contratos inativos detectados", "wallet.approval_approval_details": "Detalhes da autorização", "wallet.approval_approved_token": "Token Aprovado", diff --git a/packages/shared/src/locale/json/ru.json b/packages/shared/src/locale/json/ru.json index 6e8169242e84..0c35767e948c 100644 --- a/packages/shared/src/locale/json/ru.json +++ b/packages/shared/src/locale/json/ru.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Установить код доступа", "auth.set_password": "Установить пароль", "auth.with_biometric": "Аутентификация с помощью {biometric}", + "backing_up_desc": "Не закрывайте это окно, пока резервное копирование не завершится.", + "backing_up_title": "Резервное копирование…", "backup.address_book_labels": "Адресная книга & этикетки", "backup.all_devices": "Все устройства", "backup.backup_deleted": "Резервная копия удалена", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Проверьте пароль приложения во время резервного копирования для импорта данных.", "backup.verify_apple_account_and_icloud_drive_enabled": "Пожалуйста, проверьте вход в вашу учетную запись Apple и убедитесь, что iCloud Drive включен и авторизован для OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Пожалуйста, проверьте вход в вашу учетную запись Google и убедитесь, что Google Drive включен и авторизован для OneKey.", + "backup_restored": "Резервная копия восстановлена!", + "backup_success_toast_title": "Готово! Резервное копирование завершено", + "backup_write_to_cloud_failed": "Упс! На этот раз резервное копирование не удалось. Пожалуйста, попробуйте ещё раз", "balance_detail.button_acknowledge": "Подтвердить", "balance_detail.button_balance": "Детали баланса", "balance_detail.button_cancel": "Отмена", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Подождите {number} дней после подачи запроса на вывод средств", "earn.withdrawal_up_to_number_days": "Вывод средств может занять до {number} дней, после чего ваши заложенные активы станут доступными", "earn.withdrawn": "Изъято", + "earn_no_assets_deposited": "Активы еще не депонированы", "earn_reward_distribution_schedule": "Вознаграждения будут зачислены на ваш кошелек Arbitrum (тот же адрес, что и Ethereum) до 10 числа следующего месяца.", "edit_fee_custom_set_as_default_description": "Установить по умолчанию для всех будущих транзакций в {network}", "energy_consumed": "Потребленная энергия", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Передача данных", "firmware_update.status_validating": "Проверка", "for_reference_only": "Только для справки", + "forgot_password_no_question_mark": "Забыли пароль", "form.address_error_invalid": "Недействительный адрес", "form.address_placeholder": "Адрес или домен", "form.amount_placeholder": "Введите сумму", @@ -1101,7 +1108,7 @@ "global.allow": "Разрешить", "global.always": "Всегда", "global.an_error_occurred": "Произошла ошибка", - "global.an_error_occurred_desc": "Мы не можем выполнить ваш запрос. Пожалуйста, обновите страницу через несколько минут.", + "global.an_error_occurred_desc": "Пожалуйста, обновите страницу и повторите попытку через несколько минут.", "global.app_wallet": "Кошелек приложения", "global.apply": "Применить", "global.approval": "Подтверждение", @@ -1111,6 +1118,7 @@ "global.approve": "Утвердить", "global.apr": "APR", "global.asset": "Актив", + "global.at_least_variable_characters": "Не менее {variable} символов", "global.auto": "Авто", "global.available": "Доступно", "global.backed_up": "Сделана резервная копия", @@ -1657,6 +1665,10 @@ "global__multichain": "Мультицепочка", "global_add_money": "Пополнить баланс", "global_buy_crypto": "Купить криптовалюту", + "global_sign_in": "Войти", + "google_account_not_signed_in": "Вход в аккаунт Google не выполнен", + "google_play_services_not_available_desc": "Пожалуйста, установите Службы Google Play и войдите в свой аккаунт Google.", + "google_play_services_not_available_title": "Службы Google Play недоступны", "hardware.backup_completed": "Резервное копирование завершено!", "hardware.bluetooth_need_turned_on_error": "Bluetooth выключен", "hardware.bluetooth_not_paired_error": "Bluetooth не подключен", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Пригласите друга", "id.refer_a_friend_desc": "Пригласите друзей, чтобы заработать награды", "identical_name_asset_alert": "Мы обнаружили активы с похожими названиями в вашем кошельке. Действуйте с осторожностью.", + "import_backup_password_desc": "Пожалуйста, введите пароль для этой резервной копии.", + "import_hardware_phrases_warning": "Не импортируйте секретную фразу восстановления аппаратного кошелька. Подключить аппаратный кошелёк ↗ вместо этого", "import_phrase_or_private_key": "Импортировать сид-фразу или приватный ключ", "insufficient_fee_append_desc": "на основе макс. оценочной комиссии: {amount} {symbol}", "interact_with_contract": "Взаимодействовать с (К)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Подключение вашего аппаратного кошелька для доступа к аккаунту Lightning", "ln.authorize_access_network_error": "Аутентификация не удалась, проверьте ваше сетевое соединение и попробуйте снова", "ln.payment_received_label": "Платеж получен", + "log_out_confirmation_text": "Вы уверены, что хотите выйти из ${email}?", + "logged_out_feedback": "Вы успешно вышли из системы", "login.forgot_passcode": "Забыли код доступа?", "login.forgot_password": "Забыли пароль?", "login.welcome_message": "Добро пожаловать обратно", @@ -1983,6 +1999,11 @@ "nft.token_address": "Адрес токена", "nft.token_id": "ID токена", "no_account": "Нет аккаунта", + "no_backup_found_google_desc": "Пожалуйста, подтвердите вход в Google Drive, убедитесь, что вы используете правильную учетную запись Google, или проверьте подключение к сети", + "no_backup_found_icloud_desc": "Пожалуйста, подтвердите вход в iCloud, включите iCloud и синхронизацию Связки ключей или проверьте подключение к сети", + "no_backup_found_no_wallet": "Нет кошельков для резервного копирования", + "no_backup_found_no_wallet_desc": "Сначала создайте кошелёк", + "no_backups_found": "Резервные копии не найдены", "no_external_wallet_message": "Нет подключенных внешних кошельков. Чтобы просмотреть здесь, подключите кошелек от третьей стороны.", "no_private_key_account_message": "Нет аккаунтов с приватным ключом. Добавьте новый аккаунт для управления вашими активами.", "no_standard_wallet_desc": "Стандартный кошелёк ещё не выбран", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Вы будете получать обновления по счету в режиме реального времени, уведомления о безопасности и другую важную информацию.", "notifications.test_message_title": "Push-уведомления готовы!", "notifications.windows_notifications_permission_desc": "Получайте уведомления от приложений и других отправителей", + "older_backups_description": "Эти резервные копии были созданы в более ранних версиях. Начиная с {version}, резервные копии кошелька выполняются вручную, а данные адресной книги теперь являются частью OneKey Cloud Sync вместо облачных резервных копий.", "onboarding.activate_device": "Активируйте ваше устройство", "onboarding.activate_device_all_set": "Все готово!", "onboarding.activate_device_by_restore": "Восстановить кошелек", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Сохранить изображение", "perps.share_position_title": "Поделиться позицией", "pick_your_device": "Выберите своё устройство", + "preparing_backup_desc": "Минутку…", + "preparing_backup_title": "Подготовка резервной копии…", "prime.about_cloud_sync": "О облачной синхронизации", "prime.about_cloud_sync_description": "Узнайте, какие данные синхронизируются и как мы защищаем вашу конфиденциальность.", "prime.about_cloud_sync_included_data_title": "Синхронизируемые данные", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Поиск символа или адреса контракта", "send_token_selector.select_token": "Выберите токен", "sending_krc20_warning_text": "Отправка токенов KRC20 требует двух подтверждений на вашем аппаратном устройстве. Не отменяйте второе подтверждение, иначе KAS, отправленные при первом подтверждении, будет сложно восстановить.", + "set_new_backup_password": "Установить новый пароль резервного копирования", + "set_new_backup_password_desc": "Новый пароль резервного копирования будет использоваться для вашей следующей записи резервной копии. Существующие записи резервных копий и их пароли останутся без изменений.", "setting.floating_icon": "Плавающая иконка", "setting.floating_icon_always_display": "Всегда отображать", "setting.floating_icon_always_display_description": "Когда включено, значок OneKey плавает на краю веб-страницы, помогая быстро проверить информацию о безопасности dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Стандартный (Electrum) — работает с большинством кошельков и сервисов", "signature_format_title": "Формат подписи", "signature_type_not_supported_on_model": "Устройство {deviceModel} не поддерживает формат подписи {sigType}.", + "signed_in_feedback": "Вход выполнен успешно", "skip_firmware_check_dialog_desc": "Вы уверены, что хотите пропустить проверку? Актуальная прошивка обеспечивает лучшую защиту.", "skip_firmware_check_dialog_title": "Пропустить проверку прошивки?", "skip_verify_text": "У меня нет при себе устройства", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Не закрывайте ваше приложение во время обновления", "v4_migration.welcome_message": "OneKey 5.0 уже здесь!", "v4_migration.welcome_message_desc": "Вот как безопасно и быстро перенести ваши данные. Готовы начать?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Проверьте пароль резервной копии", + "verify_backup_password_desc": "Подтвердите пароль резервной копии в облаке, чтобы продолжить. Если вы его забыли, вы можете сбросить его, используя «Забыли пароль».", "verify_message_address_form_description": "Поддерживает: {networks}", + "view_older_backups": "Просмотреть старые резервные копии", "wallet.approval_alert_title_summary": "Обнаружено {riskyNumber} рискованных разрешений и {inactiveNumber} неактивных контрактов", "wallet.approval_approval_details": "Детали авторизации", "wallet.approval_approved_token": "Одобренный токен", diff --git a/packages/shared/src/locale/json/th_TH.json b/packages/shared/src/locale/json/th_TH.json index 84fb5797edf9..2228b246849e 100644 --- a/packages/shared/src/locale/json/th_TH.json +++ b/packages/shared/src/locale/json/th_TH.json @@ -158,6 +158,8 @@ "auth.set_passcode": "ตั้งรหัสผ่าน", "auth.set_password": "ตั้งรหัสผ่าน", "auth.with_biometric": "การยืนยันตัวตนด้วย {biometric}", + "backing_up_desc": "อย่าปิดหน้าต่างนี้จนกว่าการสำรองข้อมูลจะเสร็จสิ้น", + "backing_up_title": "กำลังสำรองข้อมูล…", "backup.address_book_labels": "สมุดที่อยู่ & ฉลาก", "backup.all_devices": "อุปกรณ์ทั้งหมด", "backup.backup_deleted": "สำรองข้อมูลถูกลบ", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "ตรวจสอบรหัสผ่านสำหรับแอปในเวลาที่ทำการสำรองข้อมูลเพื่อนำเข้าข้อมูล", "backup.verify_apple_account_and_icloud_drive_enabled": "โปรดตรวจสอบการเข้าสู่ระบบบัญชี Apple ของคุณและตรวจสอบว่าได้เปิดใช้งานและได้รับอนุญาตให้ใช้ iCloud Drive สำหรับ OneKey แล้ว", "backup.verify_google_account_and_google_drive_enabled": "โปรดตรวจสอบการเข้าสู่ระบบบัญชี Google ของคุณและตรวจสอบว่าได้เปิดใช้งานและได้รับอนุญาตให้ใช้ Google Drive สำหรับ OneKey แล้วหรือไม่", + "backup_restored": "กู้คืนข้อมูลสำรองแล้ว!", + "backup_success_toast_title": "เรียบร้อยแล้ว! การสำรองข้อมูลของคุณเสร็จสมบูรณ์", + "backup_write_to_cloud_failed": "อุ๊ย! การสำรองข้อมูลล้มเหลวครั้งนี้ โปรดลองอีกครั้ง", "balance_detail.button_acknowledge": "ยอมรับ", "balance_detail.button_balance": "รายละเอียดยอดคงเหลือ", "balance_detail.button_cancel": "ยกเลิก", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "ใช้เวลา {number} วันหลังจากส่งคำขอถอนเงิน", "earn.withdrawal_up_to_number_days": "การถอนอาจใช้เวลาสูงสุดถึง {number} วัน แล้วคุณจะสามารถใช้สินทรัพย์ที่คุณได้จัดสรรไว้", "earn.withdrawn": "ถอนออก", + "earn_no_assets_deposited": "ยังไม่มีการฝากทรัพย์สิน", "earn_reward_distribution_schedule": "รางวัลจะถูกแจกจ่ายไปยังกระเป๋าเงิน Arbitrum ของคุณ (ที่อยู่เดียวกับ Ethereum) ภายในวันที่ 10 ของเดือนหน้า", "edit_fee_custom_set_as_default_description": "ตั้งเป็นค่าเริ่มต้นสำหรับธุรกรรมทั้งหมดในอนาคตบน {network}", "energy_consumed": "พลังงานที่ใช้ไป", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "กำลังถ่ายโอนข้อมูล", "firmware_update.status_validating": "กำลังตรวจสอบ", "for_reference_only": "เพื่ออ้างอิงเท่านั้น", + "forgot_password_no_question_mark": "ลืมรหัสผ่าน", "form.address_error_invalid": "ที่อยู่ไม่ถูกต้อง", "form.address_placeholder": "ที่อยู่หรือโดเมน", "form.amount_placeholder": "กรอกจำนวน", @@ -1101,7 +1108,7 @@ "global.allow": "อนุญาต", "global.always": "เสมอ", "global.an_error_occurred": "เกิดข้อผิดพลาด", - "global.an_error_occurred_desc": "เราไม่สามารถดำเนินการตามคำขอของคุณได้ โปรดรีเฟรชหน้านี้ในอีกสักครู่", + "global.an_error_occurred_desc": "โปรดรีเฟรชและลองอีกครั้งในอีกไม่กี่นาที", "global.app_wallet": "กระเป๋าเงินแอป", "global.apply": "สมัคร", "global.approval": "การอนุมัติ", @@ -1111,6 +1118,7 @@ "global.approve": "อนุมัติ", "global.apr": "APR", "global.asset": "สินทรัพย์", + "global.at_least_variable_characters": "อย่างน้อย {variable} อักขระ", "global.auto": "อัตโนมัติ", "global.available": "พร้อมใช้งาน", "global.backed_up": "สำรองข้อมูลแล้ว", @@ -1657,6 +1665,10 @@ "global__multichain": "มัลติเชน", "global_add_money": "เติมเงิน", "global_buy_crypto": "ซื้อคริปโต", + "global_sign_in": "ลงชื่อเข้าใช้", + "google_account_not_signed_in": "ไม่ได้ลงชื่อเข้าใช้บัญชี Google", + "google_play_services_not_available_desc": "โปรดติดตั้งบริการของ Google Play และลงชื่อเข้าใช้ด้วยบัญชี Google ของคุณ", + "google_play_services_not_available_title": "ไม่มีบริการ Google Play", "hardware.backup_completed": "การสำรองข้อมูลเสร็จสิ้น!", "hardware.bluetooth_need_turned_on_error": "บลูทูธถูกปิด", "hardware.bluetooth_not_paired_error": "บลูทูธไม่ได้เชื่อมต่อ", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "แนะนำเพื่อน", "id.refer_a_friend_desc": "เชิญเพื่อนเพื่อรับรางวัล", "identical_name_asset_alert": "เราตรวจพบทรัพย์สินที่มีชื่อคล้ายกันในกระเป๋าเงินของคุณ โปรดดำเนินการด้วยความระมัดระวัง", + "import_backup_password_desc": "โปรดป้อนรหัสผ่านสำหรับข้อมูลสำรองนี้", + "import_hardware_phrases_warning": "อย่านำเข้าวลีการกู้คืนของฮาร์ดแวร์วอลเล็ตของคุณ เชื่อมต่อฮาร์ดแวร์วอลเล็ต ↗ แทน", "import_phrase_or_private_key": "นำเข้าวลีหรือกุญแจส่วนตัว", "insufficient_fee_append_desc": "ตามค่าธรรมเนียมสูงสุดที่ประมาณ: {amount} {symbol}", "interact_with_contract": "ติดต่อกับ (ถึง)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "เชื่อมต่อกระเป๋าเงินฮาร์ดแวร์ของคุณเพื่อเข้าถึงบัญชี Lightning", "ln.authorize_access_network_error": "การยืนยันตัวตนล้มเหลว โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณและลองอีกครั้ง", "ln.payment_received_label": "ได้รับการชำระเงินแล้ว", + "log_out_confirmation_text": "คุณแน่ใจหรือไม่ว่าต้องการออกจากระบบจาก ${email}?", + "logged_out_feedback": "ออกจากระบบสำเร็จ", "login.forgot_passcode": "ลืมรหัสผ่าน?", "login.forgot_password": "ลืมรหัสผ่าน?", "login.welcome_message": "ยินดีต้อนรับกลับ", @@ -1983,6 +1999,11 @@ "nft.token_address": "ที่อยู่โทเค็น", "nft.token_id": "รหัสโทเค็น", "no_account": "ไม่มีบัญชี", + "no_backup_found_google_desc": "โปรดยืนยันการลงชื่อเข้าใช้ Google Drive ตรวจสอบให้แน่ใจว่าคุณกำลังใช้บัญชี Google ที่ถูกต้อง หรือเช็กการเชื่อมต่อเครือข่ายของคุณ", + "no_backup_found_icloud_desc": "โปรดยืนยันการลงชื่อเข้าใช้ iCloud เปิดใช้งาน iCloud และการซิงค์ Keychain หรือ ตรวจสอบการเชื่อมต่อเครือข่ายของคุณ", + "no_backup_found_no_wallet": "ไม่มีวอลเล็ตให้สำรองข้อมูล", + "no_backup_found_no_wallet_desc": "โปรดสร้างกระเป๋าเงินก่อน", + "no_backups_found": "ไม่พบข้อมูลสำรอง", "no_external_wallet_message": "ไม่มีกระเป๋าเงินภายนอกที่เชื่อมต่อ ลิงก์กระเป๋าเงินของบุคคลที่สามเพื่อดูที่นี่", "no_private_key_account_message": "ไม่มีบัญชีคีย์ส่วนตัว กรุณาเพิ่มบัญชีใหม่เพื่อจัดการสินทรัพย์ของคุณ", "no_standard_wallet_desc": "ยังไม่มีวอลเล็ตมาตรฐาน", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "คุณจะได้รับการอัปเดตบัญชีแบบเรียลไทม์ การแจ้งเตือนด้านความปลอดภัย และข้อมูลสำคัญอื่นๆ", "notifications.test_message_title": "การแจ้งเตือนแบบพุชพร้อมแล้ว!", "notifications.windows_notifications_permission_desc": "รับการแจ้งเตือนจากแอปและผู้ส่งอื่น ๆ", + "older_backups_description": "ข้อมูลสำรองเหล่านี้ถูกสร้างขึ้นในเวอร์ชันก่อนหน้า ตั้งแต่รุ่น {version} เป็นต้นมา การสำรองกระเป๋าเงินจะทำด้วยตนเอง และข้อมูลสมุดที่อยู่ถูกย้ายไปเป็นส่วนหนึ่งของ OneKey Cloud Sync แทนการสำรองบนคลาวด์", "onboarding.activate_device": "เปิดใช้งานอุปกรณ์ของคุณ", "onboarding.activate_device_all_set": "ทั้งหมดพร้อมแล้ว!", "onboarding.activate_device_by_restore": "คืนค่ากระเป๋าเงิน", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "บันทึกรูปภาพ", "perps.share_position_title": "แชร์ตำแหน่ง", "pick_your_device": "เลือกอุปกรณ์ของคุณ", + "preparing_backup_desc": "สักครู่…", + "preparing_backup_title": "กำลังเตรียมสำรองข้อมูล…", "prime.about_cloud_sync": "เกี่ยวกับคลาวด์ซิงค์", "prime.about_cloud_sync_description": "เรียนรู้ว่าข้อมูลใดบ้างที่ถูกซิงค์และเราปกป้องความเป็นส่วนตัวของคุณอย่างไร", "prime.about_cloud_sync_included_data_title": "ข้อมูลที่ซิงค์", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "ค้นหาสัญลักษณ์หรือที่อยู่สัญญา", "send_token_selector.select_token": "เลือกโทเค็น", "sending_krc20_warning_text": "การส่งโทเค็น KRC20 ต้องการการยืนยันสองครั้งบนฮาร์ดแวร์ของคุณ อย่ายกเลิกการยืนยันครั้งที่สอง มิฉะนั้น KAS ที่ส่งในการยืนยันครั้งแรกจะยากต่อการกู้คืน", + "set_new_backup_password": "ตั้งรหัสผ่านสำรองใหม่", + "set_new_backup_password_desc": "รหัสผ่านสําหรับการสํารองข้อมูลใหม่จะถูกใช้กับระเบียนการสํารองข้อมูลครั้งถัดไปของคุณ ระเบียนการสํารองข้อมูลที่มีอยู่และรหัสผ่านของระเบียนเหล่านั้นจะยังคงเหมือนเดิม", "setting.floating_icon": "ไอคอนลอย", "setting.floating_icon_always_display": "แสดงเสมอ", "setting.floating_icon_always_display_description": "เมื่อเปิดใช้งาน ไอคอน OneKey จะลอยอยู่ที่ขอบของหน้าเว็บ ช่วยให้คุณตรวจสอบข้อมูลความปลอดภัยของ dApps ได้อย่างรวดเร็ว", @@ -3080,6 +3106,7 @@ "signature_format_standard": "มาตรฐาน (Electrum) — ใช้งานได้กับกระเป๋าเงินและบริการส่วนใหญ่", "signature_format_title": "รูปแบบลายเซ็น", "signature_type_not_supported_on_model": "อุปกรณ์ {deviceModel} ไม่รองรับรูปแบบลายเซ็น {sigType}", + "signed_in_feedback": "ลงชื่อเข้าใช้สำเร็จ", "skip_firmware_check_dialog_desc": "แน่ใจหรือไม่ว่าต้องการข้ามการตรวจสอบ? การใช้เฟิร์มแวร์เวอร์ชันล่าสุดจะช่วยให้คุณได้รับการป้องกันที่ดีที่สุด", "skip_firmware_check_dialog_title": "ข้ามการตรวจสอบเฟิร์มแวร์หรือไม่?", "skip_verify_text": "ฉันไม่มีอุปกรณ์ของฉันอยู่กับฉัน", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "อย่าปิดแอปของคุณระหว่างการอัปเดต", "v4_migration.welcome_message": "OneKey 5.0 มาแล้ว!", "v4_migration.welcome_message_desc": "นี่คือวิธีการโยกย้ายข้อมูลของคุณอย่างปลอดภัยและรวดเร็ว พร้อมเริ่มแล้วหรือยัง?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "ยืนยันรหัสผ่านสํารอง", + "verify_backup_password_desc": "โปรดยืนยันรหัสผ่านสําหรับการสํารองข้อมูลบนคลาวด์เพื่อดำเนินการต่อ หากคุณลืมรหัสผ่าน คุณสามารถรีเซ็ตได้โดยใช้ \"ลืมรหัสผ่าน\"", "verify_message_address_form_description": "รองรับ: {networks}", + "view_older_backups": "ดูการสำรองข้อมูลที่เก่ากว่า", "wallet.approval_alert_title_summary": "ตรวจพบการอนุมัติที่มีความเสี่ยง {riskyNumber} รายการ และสัญญาที่ไม่มีการใช้งาน {inactiveNumber} ฉบับ", "wallet.approval_approval_details": "รายละเอียดการอนุญาต", "wallet.approval_approved_token": "โ���เค็นที่ได้รับอนุมัติ", diff --git a/packages/shared/src/locale/json/uk_UA.json b/packages/shared/src/locale/json/uk_UA.json index a6eb4bf196aa..f33aa94a3b8e 100644 --- a/packages/shared/src/locale/json/uk_UA.json +++ b/packages/shared/src/locale/json/uk_UA.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Встановити код доступу", "auth.set_password": "Встановити пароль", "auth.with_biometric": "Аутентифікація за допомогою {biometric}", + "backing_up_desc": "Не закривайте це вікно, доки резервне копіювання не завершиться.", + "backing_up_title": "Резервне копіювання…", "backup.address_book_labels": "Адресна книга & етикетки", "backup.all_devices": "Всі пристрої", "backup.backup_deleted": "Резервну копію видалено", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Перевірте пароль програми під час резервного копіювання для імпорту даних.", "backup.verify_apple_account_and_icloud_drive_enabled": "Будь ласка, перевірте вхід у свій обліковий запис Apple та переконайтеся, що iCloud Drive включено та авторизовано для OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Будь ласка, перевірте вхід у свій акаунт Google та переконайтеся, що Google Drive включено та авторизовано для OneKey.", + "backup_restored": "Резервну копію відновлено!", + "backup_success_toast_title": "Готово! Резервне копіювання завершено", + "backup_write_to_cloud_failed": "Ой! Резервне копіювання не вдалося. Будь ласка, спробуйте ще раз", "balance_detail.button_acknowledge": "Підтвердити", "balance_detail.button_balance": "Деталі балансу", "balance_detail.button_cancel": "Скасувати", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Займає {number} днів після подання запиту на зняття коштів", "earn.withdrawal_up_to_number_days": "Виведення коштів може тривати до {number} днів, а потім ваши забезпечені активи стануть доступними", "earn.withdrawn": "Вилучено", + "earn_no_assets_deposited": "Активів ще немає внесено", "earn_reward_distribution_schedule": "Винагороди будуть розподілені на ваш гаманець Arbitrum (та сама адреса, що й Ethereum) до 10-го числа наступного місяця.", "edit_fee_custom_set_as_default_description": "Встановити як типове для всіх майбутніх транзакцій у мережі {network}", "energy_consumed": "Енергія, що споживається", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Передача даних", "firmware_update.status_validating": "Перевірка", "for_reference_only": "Тільки для довідки", + "forgot_password_no_question_mark": "Забули пароль", "form.address_error_invalid": "Неправильна адреса", "form.address_placeholder": "Адреса або домен", "form.amount_placeholder": "Введіть суму", @@ -1101,7 +1108,7 @@ "global.allow": "Дозволити", "global.always": "Завжди", "global.an_error_occurred": "Сталася помилка", - "global.an_error_occurred_desc": "Ми не можемо виконати ваш запит. Будь ласка, оновіть сторінку через кілька хвилин.", + "global.an_error_occurred_desc": "Будь ласка, оновіть сторінку та спробуйте ще раз через кілька хвилин.", "global.app_wallet": "Гаманець в додатку", "global.apply": "Застосувати", "global.approval": "Підтвердження", @@ -1111,6 +1118,7 @@ "global.approve": "Підтвердити", "global.apr": "APR", "global.asset": "Актив", + "global.at_least_variable_characters": "Щонайменше {variable} символів", "global.auto": "Авто", "global.available": "Доступно", "global.backed_up": "Резервне копіювання", @@ -1657,6 +1665,10 @@ "global__multichain": "Мультиланцюг", "global_add_money": "Додати гроші", "global_buy_crypto": "Купити криптовалюту", + "global_sign_in": "Увійти", + "google_account_not_signed_in": "Обліковий запис Google не ввійшов у систему", + "google_play_services_not_available_desc": "Будь ласка, встановіть Google Play Services і увійдіть за допомогою свого облікового запису Google.", + "google_play_services_not_available_title": "Сервіси Google Play недоступні", "hardware.backup_completed": "Резервне копіювання завершено!", "hardware.bluetooth_need_turned_on_error": "Bluetooth вимкнено", "hardware.bluetooth_not_paired_error": "Bluetooth не спарений", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Запросити друга", "id.refer_a_friend_desc": "Запросіть друзів, щоб заробити винагороди", "identical_name_asset_alert": "Ми виявили активи з подібними назвами у вашому гаманці. Будьте обережні.", + "import_backup_password_desc": "Будь ласка, введіть пароль для цієї резервної копії.", + "import_hardware_phrases_warning": "Не імпортуйте фразу відновлення вашого апаратного гаманця. Підключити апаратний гаманець ↗ натомість", "import_phrase_or_private_key": "Імпортувати фразу або приватний ключ", "insufficient_fee_append_desc": "на основі макс. оціненої комісії: {amount} {symbol}", "interact_with_contract": "Взаємодіяти з (До)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Підключення вашого апаратного гаманця для доступу до рахунку Lightning", "ln.authorize_access_network_error": "Помилка аутентифікації, перевірте ваше мережеве з'єднання та спробуйте знову", "ln.payment_received_label": "Отриманої компенсації", + "log_out_confirmation_text": "Ви впевнені, що хочете вийти з облікового запису ${email}?", + "logged_out_feedback": "Вихід виконано успішно", "login.forgot_passcode": "Забули код доступу?", "login.forgot_password": "Забули пароль?", "login.welcome_message": "Ласкаво просимо назад", @@ -1983,6 +1999,11 @@ "nft.token_address": "Адреса токена", "nft.token_id": "ID токену", "no_account": "Немає облікового запису", + "no_backup_found_google_desc": "Будь ласка, перевірте свій вхід у Google Drive, переконайтеся, що ви використовуєте правильний обліковий запис Google, або перевірте підключення до мережі", + "no_backup_found_icloud_desc": "Будь ласка, підтвердьте свій вхід в iCloud, увімкніть синхронізацію iCloud і Keychain або перевірте підключення до мережі", + "no_backup_found_no_wallet": "Немає гаманців для резервного копіювання", + "no_backup_found_no_wallet_desc": "Будь ласка, спочатку створіть гаманець", + "no_backups_found": "Резервні копії не знайдено", "no_external_wallet_message": "Жодні зовнішні гаманці не підключені. Підключіть сторонній гаманець, щоб переглянути його тут.", "no_private_key_account_message": "Немає приватних ключових облікових записів. Додайте новий обліковий запис, щоб керувати своїми активами.", "no_standard_wallet_desc": "Ще немає стандартного гаманця", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Ви будете отримувати оновлення рахунку в режимі реального часу, сповіщення про безпеку та іншу важливу інформацію.", "notifications.test_message_title": "Сповіщення готові!", "notifications.windows_notifications_permission_desc": "Отримуйте сповіщення від додатків та інших відправників", + "older_backups_description": "Ці резервні копії були створені в попередніх версіях. Починаючи з версії {version}, резервні копії гаманця створюються вручну, а дані адресної книги тепер є частиною OneKey Cloud Sync замість хмарних резервних копій.", "onboarding.activate_device": "Активуйте свій пристрій", "onboarding.activate_device_all_set": "Все готово!", "onboarding.activate_device_by_restore": "Відновити гаманець", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Зберегти зображення", "perps.share_position_title": "Поділитися позицією", "pick_your_device": "Виберіть свій пристрій", + "preparing_backup_desc": "Зачекайте…", + "preparing_backup_title": "Підготовка резервної копії…", "prime.about_cloud_sync": "Про хмарну синхронізацію", "prime.about_cloud_sync_description": "Дізнайтеся, які дані синхронізуються та як ми захищаємо вашу конфіденційність.", "prime.about_cloud_sync_included_data_title": "Дані для синхронізації", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Шукати символ або адресу контракту", "send_token_selector.select_token": "Вибрати токен", "sending_krc20_warning_text": "Відправлення токенів KRC20 вимагає двох підтверджень на вашому апаратному пристрої. Не скасовуйте друге підтвердження, інакше KAS, відправлені при першому підтвердженні, буде важко відновити.", + "set_new_backup_password": "Встановити новий пароль резервної копії", + "set_new_backup_password_desc": "Новий пароль резервної копії використовуватиметься для вашого наступного запису резервної копії. Існуючі записи резервних копій та їхні паролі залишаться незмінними.", "setting.floating_icon": "Плаваючий значок", "setting.floating_icon_always_display": "Завжди відображати", "setting.floating_icon_always_display_description": "Коли ввімкнено, значок OneKey плаває на краю веб-сторінки, допомагаючи швидко перевірити інформацію про безпеку dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Стандартний (Electrum) — Працює з більшістю гаманців і сервісів", "signature_format_title": "Формат підпису", "signature_type_not_supported_on_model": "{sigType} не підтримується на пристрої: {deviceModel}", + "signed_in_feedback": "Успішно виконано вхід", "skip_firmware_check_dialog_desc": "Ви впевнені, що хочете пропустити перевірку? Використання актуальної версії мікропрограми забезпечує найкращий захист.", "skip_firmware_check_dialog_title": "Пропустити перевірку прошивки?", "skip_verify_text": "У мене немає приладу зі мною", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Не закривайте свою програму під час оновлення", "v4_migration.welcome_message": "OneKey 5.0 вже тут!", "v4_migration.welcome_message_desc": "Ось як безпечно та швидко перенести ваші дані. Готові почати?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Підтвердьте пароль резервної копії", + "verify_backup_password_desc": "Будь ласка, підтвердьте пароль резервного копіювання в хмарі, щоб продовжити. Якщо ви його забули, ви можете скинути його за допомогою функції «Забули пароль».", "verify_message_address_form_description": "Підтримує: {networks}", + "view_older_backups": "Переглянути старіші резервні копії", "wallet.approval_alert_title_summary": "Виявлено {riskyNumber} ризикованих дозволів та {inactiveNumber} неактивних контрактів", "wallet.approval_approval_details": "Деталі авторизації", "wallet.approval_approved_token": "Схвалений токен", diff --git a/packages/shared/src/locale/json/vi.json b/packages/shared/src/locale/json/vi.json index 8b42b6f45655..fc178e37395e 100644 --- a/packages/shared/src/locale/json/vi.json +++ b/packages/shared/src/locale/json/vi.json @@ -158,6 +158,8 @@ "auth.set_passcode": "Thiết lập mã khóa", "auth.set_password": "Đặt mật khẩu", "auth.with_biometric": "Xác thực bằng {biometric}", + "backing_up_desc": "Đừng đóng cửa sổ này cho đến khi quá trình sao lưu hoàn tất.", + "backing_up_title": "Đang sao lưu…", "backup.address_book_labels": "Sổ địa chỉ & nhãn", "backup.all_devices": "Tất cả các thiết bị", "backup.backup_deleted": "Đã xóa bản sao lưu", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "Xác minh mật khẩu Ứng dụng tại thời điểm sao lưu để Nhập dữ liệu.", "backup.verify_apple_account_and_icloud_drive_enabled": "Vui lòng xác minh đăng nhập tài khoản Apple của bạn và đảm bảo iCloud Drive được kích hoạt và ủy quyền cho OneKey.", "backup.verify_google_account_and_google_drive_enabled": "Vui lòng xác minh đăng nhập tài khoản Google của bạn và đảm bảo Google Drive được kích hoạt và ủy quyền cho OneKey.", + "backup_restored": "Đã khôi phục bản sao lưu!", + "backup_success_toast_title": "Mọi thứ đã sẵn sàng! Sao lưu của bạn đã hoàn tất", + "backup_write_to_cloud_failed": "Rất tiếc! Sao lưu đã thất bại lần này. Vui lòng thử lại", "balance_detail.button_acknowledge": "Xác nhận", "balance_detail.button_balance": "Chi tiết Số dư", "balance_detail.button_cancel": "Hủy", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "Mất {number} ngày sau khi gửi yêu cầu rút tiền", "earn.withdrawal_up_to_number_days": "Việc rút tiền có thể mất đến {number} ngày, sau đó tài sản của bạn đã cược sẽ trở nên khả dụng", "earn.withdrawn": "Đã rút lại", + "earn_no_assets_deposited": "Chưa có tài sản nào được gửi", "earn_reward_distribution_schedule": "Phần thưởng sẽ được phân phối vào ví Arbitrum của bạn (cùng địa chỉ với Ethereum) vào ngày 10 tháng sau.", "edit_fee_custom_set_as_default_description": "Đặt làm mặc định cho tất cả các giao dịch trong tương lai trên {network}", "energy_consumed": "Năng lượng tiêu thụ", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "Chuyển dữ liệu", "firmware_update.status_validating": "Xác thực", "for_reference_only": "Chỉ để tham khảo", + "forgot_password_no_question_mark": "Quên mật khẩu", "form.address_error_invalid": "Địa chỉ không hợp lệ", "form.address_placeholder": "Địa chỉ hoặc tên miền", "form.amount_placeholder": "Nhập số tiền", @@ -1101,7 +1108,7 @@ "global.allow": "Cho phép", "global.always": "Luôn luôn", "global.an_error_occurred": "Đã xảy ra lỗi", - "global.an_error_occurred_desc": "Chúng tôi không thể hoàn thành yêu cầu của bạn. Vui lòng làm mới trang sau vài phút.", + "global.an_error_occurred_desc": "Vui lòng làm mới và thử lại sau vài phút.", "global.app_wallet": "Ví ứng dụng", "global.apply": "Áp dụng", "global.approval": "Phê duyệt", @@ -1111,6 +1118,7 @@ "global.approve": "Chấp nhận", "global.apr": "APR", "global.asset": "Tài sản", + "global.at_least_variable_characters": "Ít nhất {variable} ký tự", "global.auto": "Tự động", "global.available": "Có sẵn", "global.backed_up": "Đã sao lưu", @@ -1657,6 +1665,10 @@ "global__multichain": "Đa chuỗi", "global_add_money": "Nạp tiền", "global_buy_crypto": "Mua tiền mã hoá", + "global_sign_in": "Đăng nhập", + "google_account_not_signed_in": "Chưa đăng nhập tài khoản Google", + "google_play_services_not_available_desc": "Vui lòng cài đặt Dịch vụ của Google Play và đăng nhập bằng tài khoản Google của bạn.", + "google_play_services_not_available_title": "Dịch vụ của Google Play không khả dụng", "hardware.backup_completed": "Sao lưu hoàn tất!", "hardware.bluetooth_need_turned_on_error": "Bluetooth đang tắt", "hardware.bluetooth_not_paired_error": "Bluetooth chưa được ghép nối", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "Giới thiệu bạn bè", "id.refer_a_friend_desc": "Mời bạn bè để nhận phần thưởng", "identical_name_asset_alert": "Chúng tôi đã phát hiện thấy tài sản có tên tương tự trong ví của bạn. Hãy tiếp tục một cách thận trọng.", + "import_backup_password_desc": "Vui lòng nhập mật khẩu cho bản sao lưu này.", + "import_hardware_phrases_warning": "Đừng nhập cụm từ khôi phục của ví phần cứng. Kết nối ví phần cứng ↗ thay vào đó", "import_phrase_or_private_key": "Nhập cụm từ hoặc khóa riêng tư", "insufficient_fee_append_desc": "dựa trên phí ước tính tối đa: {amount} {symbol}", "interact_with_contract": "Tương tác với (Đến)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "Kết nối ví cứng của bạn để truy cập vào tài khoản Lightning", "ln.authorize_access_network_error": "Xác thực không thành công, kiểm tra kết nối mạng của bạn và thử lại", "ln.payment_received_label": "Thanh toán nhận được", + "log_out_confirmation_text": "Bạn có chắc muốn đăng xuất khỏi ${email} không?", + "logged_out_feedback": "Đăng xuất thành công", "login.forgot_passcode": "Quên mã khóa?", "login.forgot_password": "Quên mật khẩu?", "login.welcome_message": "Chào mừng trở lại", @@ -1983,6 +1999,11 @@ "nft.token_address": "Địa chỉ Token", "nft.token_id": "ID Token", "no_account": "Không có tài khoản", + "no_backup_found_google_desc": "Vui lòng xác minh việc đăng nhập Google Drive của bạn, đảm bảo bạn đang dùng đúng tài khoản Google, hoặc kiểm tra kết nối mạng của bạn", + "no_backup_found_icloud_desc": "Vui lòng xác minh việc đăng nhập iCloud của bạn, bật iCloud và đồng bộ Chuỗi khóa, hoặc kiểm tra kết nối mạng của bạn", + "no_backup_found_no_wallet": "Không có ví nào để sao lưu", + "no_backup_found_no_wallet_desc": "Vui lòng tạo ví trước", + "no_backups_found": "Không tìm thấy bản sao lưu", "no_external_wallet_message": "Không có ví bên ngoài nào được kết nối. Hãy liên kết một ví của bên thứ ba để xem ở đây.", "no_private_key_account_message": "Không có tài khoản khóa riêng tư. Thêm một tài khoản mới để quản lý tài sản của bạn.", "no_standard_wallet_desc": "Chưa có ví tiêu chuẩn", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "Bạn sẽ nhận được cập nhật tài khoản theo thời gian thực, cảnh báo bảo mật và nhiều thông tin quan trọng khác.", "notifications.test_message_title": "Thông báo đẩy đã sẵn sàng!", "notifications.windows_notifications_permission_desc": "Nhận thông báo từ các ứng dụng và người gửi khác", + "older_backups_description": "Các bản sao lưu này được tạo bằng các phiên bản trước. Từ {version}, việc sao lưu ví là thủ công, và dữ liệu sổ địa chỉ hiện là một phần của OneKey Cloud Sync thay vì các bản sao lưu đám mây.", "onboarding.activate_device": "Kích hoạt thiết bị của bạn", "onboarding.activate_device_all_set": "Tất cả đã sẵn sàng!", "onboarding.activate_device_by_restore": "Khôi phục ví", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "Lưu hình ảnh", "perps.share_position_title": "Chia sẻ vị trí", "pick_your_device": "Chọn thiết bị của bạn", + "preparing_backup_desc": "Chờ một chút…", + "preparing_backup_title": "Đang chuẩn bị sao lưu…", "prime.about_cloud_sync": "Về Cloud Sync", "prime.about_cloud_sync_description": "Tìm hiểu về dữ liệu được đồng bộ và cách chúng tôi bảo vệ quyền riêng tư của bạn.", "prime.about_cloud_sync_included_data_title": "Dữ liệu được đồng bộ", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "Tìm kiếm biểu tượng hoặc địa chỉ hợp đồng", "send_token_selector.select_token": "Chọn Token", "sending_krc20_warning_text": "Gửi token KRC20 yêu cầu hai lần xác nhận trên phần cứng của bạn. Không hủy lần xác nhận thứ hai, nếu không KAS đã gửi trong lần xác nhận đầu tiên sẽ khó khôi phục.", + "set_new_backup_password": "Đặt mật khẩu sao lưu mới", + "set_new_backup_password_desc": "Mật khẩu sao lưu mới sẽ được dùng cho bản ghi sao lưu tiếp theo của bạn. Các bản ghi sao lưu hiện có và mật khẩu của chúng sẽ giữ nguyên.", "setting.floating_icon": "Biểu tượng nổi", "setting.floating_icon_always_display": "Luôn hiển thị", "setting.floating_icon_always_display_description": "Khi được bật, biểu tượng OneKey nổi trên cạnh trang web, giúp bạn nhanh chóng kiểm tra thông tin bảo mật của dApps.", @@ -3080,6 +3106,7 @@ "signature_format_standard": "Tiêu chuẩn (Electrum) — Hoạt động với hầu hết các ví và dịch vụ", "signature_format_title": "Định dạng chữ ký", "signature_type_not_supported_on_model": "Thiết bị {deviceModel} không hỗ trợ định dạng chữ ký {sigType}", + "signed_in_feedback": "Đăng nhập thành công", "skip_firmware_check_dialog_desc": "Bạn có chắc muốn bỏ qua bước kiểm tra không? Sử dụng firmware mới nhất sẽ giúp bạn được bảo vệ tốt nhất.", "skip_firmware_check_dialog_title": "Bỏ qua kiểm tra firmware?", "skip_verify_text": "Tôi không mang theo thiết bị của mình", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "Đừng tắt ứng dụng của bạn trong quá trình cập nhật", "v4_migration.welcome_message": "OneKey 5.0 đã ra mắt!", "v4_migration.welcome_message_desc": "Đây là cách di chuyển dữ liệu của bạn một cách an toàn và nhanh chóng. Bạn đã sẵn sàng bắt đầu chưa?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "Xác minh mật khẩu sao lưu", + "verify_backup_password_desc": "Vui lòng xác nhận mật khẩu sao lưu đám mây của bạn để tiếp tục. Nếu bạn quên, bạn có thể đặt lại bằng \"Quên mật khẩu\".", "verify_message_address_form_description": "Hỗ trợ: {networks}", + "view_older_backups": "Xem các bản sao lưu cũ hơn", "wallet.approval_alert_title_summary": "Đã phát hiện {riskyNumber} phê duyệt rủi ro và {inactiveNumber} hợp đồng không hoạt động", "wallet.approval_approval_details": "Chi tiết ủy quyền", "wallet.approval_approved_token": "Token đã được phê duyệt", diff --git a/packages/shared/src/locale/json/zh_CN.json b/packages/shared/src/locale/json/zh_CN.json index 23adc1f425c6..c9cd124fe587 100644 --- a/packages/shared/src/locale/json/zh_CN.json +++ b/packages/shared/src/locale/json/zh_CN.json @@ -158,6 +158,8 @@ "auth.set_passcode": "设置密码", "auth.set_password": "设置密码", "auth.with_biometric": "使用 {biometric} 进行身份验证", + "backing_up_desc": "在备份完成之前,请勿关闭此窗口。", + "backing_up_title": "正在备份…", "backup.address_book_labels": "地址簿和标签", "backup.all_devices": "所有设备", "backup.backup_deleted": "备份已删除", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "验证在备份时使用的应用密码以导入数据", "backup.verify_apple_account_and_icloud_drive_enabled": "请验证您的 Apple 账户登录,并确保已在 iCloud Drive 中授权给 OneKey 必要的权限", "backup.verify_google_account_and_google_drive_enabled": "请验证您的 Google 账户登录,并确保已在 Google Drive 中授权给 OneKey 必要的权限", + "backup_restored": "备份已恢复!", + "backup_success_toast_title": "备份已完成", + "backup_write_to_cloud_failed": "备份失败,请重试", "balance_detail.button_acknowledge": "确认", "balance_detail.button_balance": "余额详情", "balance_detail.button_cancel": "取消", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "发出赎回请求后,可能等待 {number} 天", "earn.withdrawal_up_to_number_days": "赎回可能需要多达 {number} 天,然后您的抵押资产将会变得可用", "earn.withdrawn": "已赎回", + "earn_no_assets_deposited": "目前尚未认购任何资产", "earn_reward_distribution_schedule": "奖励将于下个月 10 号之前发放到您的 Arbitrum 钱包(与以太坊相同的地址)。", "edit_fee_custom_set_as_default_description": "设为 {network} 网络未来交易的默认值", "energy_consumed": "能量消耗", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "传输数据", "firmware_update.status_validating": "验证中", "for_reference_only": "仅供参考", + "forgot_password_no_question_mark": "忘记密码", "form.address_error_invalid": "无效地址", "form.address_placeholder": "地址或域名", "form.amount_placeholder": "输入金额", @@ -1101,7 +1108,7 @@ "global.allow": "允许", "global.always": "总是", "global.an_error_occurred": "发生了错误", - "global.an_error_occurred_desc": "我们无法完成您的请求。请在几分钟后刷新页面。", + "global.an_error_occurred_desc": "请刷新页面或稍后再试。", "global.app_wallet": "App 钱包", "global.apply": "应用", "global.approval": "授权", @@ -1111,6 +1118,7 @@ "global.approve": "授权", "global.apr": "APR", "global.asset": "资产", + "global.at_least_variable_characters": "至少 {variable} 个字符", "global.auto": "自动", "global.available": "可用", "global.backed_up": "已备份", @@ -1657,6 +1665,10 @@ "global__multichain": "多链", "global_add_money": "充值", "global_buy_crypto": "购买加密货币", + "global_sign_in": "登录", + "google_account_not_signed_in": "Google 账号未登录", + "google_play_services_not_available_desc": "请安装 Google Play 服务并使用您的 Google 账号登录。", + "google_play_services_not_available_title": "Google Play 服务不可用", "hardware.backup_completed": "备份完成!", "hardware.bluetooth_need_turned_on_error": "蓝牙已关闭", "hardware.bluetooth_not_paired_error": "蓝牙未配对", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "推荐奖励", "id.refer_a_friend_desc": "邀请朋友赚取奖励", "identical_name_asset_alert": "检测到钱包中存在相同或相似名称的资产,请谨慎操作,避免损失。", + "import_backup_password_desc": "请输入此备份的密码。", + "import_hardware_phrases_warning": "不要导入硬件钱包的助记词,请使用连接硬件钱包 ↗", "import_phrase_or_private_key": "导入助记词或私钥", "insufficient_fee_append_desc": "基于最大預估费用:{amount} {symbol}", "interact_with_contract": "交互地址 (至)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "连接您的硬件钱包以访问 Lightning 账户", "ln.authorize_access_network_error": "身份认证失败,请检查网络连接后重试", "ln.payment_received_label": "已收到付款", + "log_out_confirmation_text": "您确定要退出 ${email} 的登录吗?", + "logged_out_feedback": "已成功退出登录", "login.forgot_passcode": "忘记密码?", "login.forgot_password": "忘记密码?", "login.welcome_message": "欢迎回来", @@ -1983,6 +1999,11 @@ "nft.token_address": "代币地址", "nft.token_id": "代币 ID", "no_account": "没有账户", + "no_backup_found_google_desc": "请确保您的 Google Drive 已登录并使用的是正确的 Google 账户,或检查您的网络连接", + "no_backup_found_icloud_desc": "请验证您的 iCloud 登录信息,启用 iCloud 和钥匙串同步,或检查您的网络连接", + "no_backup_found_no_wallet": "没有可备份的钱包", + "no_backup_found_no_wallet_desc": "请先创建钱包", + "no_backups_found": "未找到备份", "no_external_wallet_message": "没有连接外部钱包。连接第三方钱包后,可在此查看。", "no_private_key_account_message": "目前没有私钥账户。添加新账户以管理您的资产。", "no_standard_wallet_desc": "尚无标准钱包", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "您将及时收到账户动态、安全提醒及更多重要信息。", "notifications.test_message_title": "推送通知已就绪!", "notifications.windows_notifications_permission_desc": "获取来自应用和其他发送者的通知", + "older_backups_description": "这些是旧版本创建的备份。从 {version} 起:钱包需要手动备份,地址簿已整合到 OneKey Cloud 同步,不再随云端备份保存。", "onboarding.activate_device": "激活您的设备", "onboarding.activate_device_all_set": "准备就绪", "onboarding.activate_device_by_restore": "恢复钱包", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "保存图片", "perps.share_position_title": "分享仓位", "pick_your_device": "选择您的设备", + "preparing_backup_desc": "请稍候…", + "preparing_backup_title": "正在准备备份…", "prime.about_cloud_sync": "关于云端同步", "prime.about_cloud_sync_description": "了解会同步哪些数据,以及我们如何保护您的隐私。", "prime.about_cloud_sync_included_data_title": "同步数据", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "搜索代币名称或合约地址", "send_token_selector.select_token": "选择代币", "sending_krc20_warning_text": "发送 KRC20 代币需在硬件确认两次。请勿取消第二次确认,否则第一次确认发出的 KAS 将难以找回。", + "set_new_backup_password": "设置新备份密码", + "set_new_backup_password_desc": "新的备份密码将用于您的下一次备份记录。现有的备份记录及其密码将保持不变。", "setting.floating_icon": "悬浮球", "setting.floating_icon_always_display": "始终显示", "setting.floating_icon_always_display_description": "启用后,OneKey 图标会浮动在网页边缘,帮助您快速检查 dApp 的安全信息。", @@ -3080,6 +3106,7 @@ "signature_format_standard": "标准 (Electrum) — 兼容大多数钱包和服务", "signature_format_title": "签名格式", "signature_type_not_supported_on_model": "设备 {deviceModel} 不支持 {sigType} 签名格式", + "signed_in_feedback": "登录成功", "skip_firmware_check_dialog_desc": "确定要跳过检查吗?使用最新固件可为您提供最佳保护。", "skip_firmware_check_dialog_title": "跳过固件检查?", "skip_verify_text": "设备不在我身边", @@ -3280,7 +3307,7 @@ "swap_page.toast.swap_failed_detail": "{number} {tokenSymbol} → {number} {tokenSymbol}", "swap_page.toast.swap_successful": "交易成功", "swap_page.toast.swap_successful_detail": "{number} {symbol} → {number} {symbol}", - "swap_page.toast.taproot_unsupported": "ThorSwap 目前不支持 Taproot 格式的 BTC 地址。", + "swap_page.toast.taproot_unsupported": "ThorSwap 目前不支持 Taproot 格式的 BTC 地址", "swap_page.toast.token_not_supported": "该代币不支持", "swap_page.usd_value": "${number}", "swap_process.build_and_estimate_tx": "构建和预估交易", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "在更新过程中,请不要关闭您的应用程序", "v4_migration.welcome_message": "OneKey 5.0 已到来", "v4_migration.welcome_message_desc": "让我们安全快速地迁移您的数据。准备好了吗?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "验证备份密码", + "verify_backup_password_desc": "请确认您的云备份密码以继续。如果您忘记了密码,可以使用「忘记密码」进行重置。", "verify_message_address_form_description": "支持:{networks}", + "view_older_backups": "查看历史备份", "wallet.approval_alert_title_summary": "检测到 {riskyNumber} 项风险授权和 {inactiveNumber} 个未活跃的合约", "wallet.approval_approval_details": "授权详情", "wallet.approval_approved_token": "已批准的代币", diff --git a/packages/shared/src/locale/json/zh_HK.json b/packages/shared/src/locale/json/zh_HK.json index 605c009fb150..dd25f86366a4 100644 --- a/packages/shared/src/locale/json/zh_HK.json +++ b/packages/shared/src/locale/json/zh_HK.json @@ -158,6 +158,8 @@ "auth.set_passcode": "設置密碼", "auth.set_password": "設置密碼", "auth.with_biometric": "使用 {biometric} 進行身份驗證", + "backing_up_desc": "在備份完成之前,請勿關閉此視窗。", + "backing_up_title": "正在備份…", "backup.address_book_labels": "地址簿 & 標籤", "backup.all_devices": "所有裝置", "backup.backup_deleted": "備份已刪除", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "驗證在備份時使用的應用密碼以導入數據", "backup.verify_apple_account_and_icloud_drive_enabled": "請驗證您的 Apple 賬戶登錄,並確保已在 iCloud Drive 中授權給 OneKey 必要的權限", "backup.verify_google_account_and_google_drive_enabled": "請驗證您的 Google 賬戶登錄,並確保已在 Google Drive 中授權給 OneKey 必要的權限", + "backup_restored": "備份已還原!", + "backup_success_toast_title": "備份已完成", + "backup_write_to_cloud_failed": "備份失敗。請再試一次", "balance_detail.button_acknowledge": "確認", "balance_detail.button_balance": "餘額詳情", "balance_detail.button_cancel": "取消", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "提出贖回請求後,可能等待 {number} 天", "earn.withdrawal_up_to_number_days": "贖回可能需要多達 {number} 天,然後您的抵押資產將會變得可用", "earn.withdrawn": "已贖回", + "earn_no_assets_deposited": "目前尚未認購任何資產", "earn_reward_distribution_schedule": "獎勵將於下個月 10 號之前發放到您的 Arbitrum 錢包(與以太坊相同的地址)。", "edit_fee_custom_set_as_default_description": "設為 {network} 網路未來交易的預設值", "energy_consumed": "能量消耗", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "傳輸數據", "firmware_update.status_validating": "驗證中", "for_reference_only": "僅供參考", + "forgot_password_no_question_mark": "忘記密碼", "form.address_error_invalid": "無效地址", "form.address_placeholder": "地址或域名", "form.amount_placeholder": "輸入金額", @@ -1101,7 +1108,7 @@ "global.allow": "准許", "global.always": "總是", "global.an_error_occurred": "發生了錯誤", - "global.an_error_occurred_desc": "我們無法完成您的請求。請在幾分鐘後刷新頁面。", + "global.an_error_occurred_desc": "請刷新頁面或稍後再試。", "global.app_wallet": "App 錢包", "global.apply": "應用", "global.approval": "授權", @@ -1111,6 +1118,7 @@ "global.approve": "授權", "global.apr": "APR", "global.asset": "資產", + "global.at_least_variable_characters": "至少 {variable} 個字元", "global.auto": "自動", "global.available": "可用", "global.backed_up": "已備份", @@ -1657,6 +1665,10 @@ "global__multichain": "多鏈", "global_add_money": "儲值", "global_buy_crypto": "購買加密貨幣", + "global_sign_in": "登入", + "google_account_not_signed_in": "Google 帳戶未登入", + "google_play_services_not_available_desc": "請安裝 Google Play 服務並使用您的 Google 帳戶登入。", + "google_play_services_not_available_title": "Google Play 服務無法使用", "hardware.backup_completed": "備份完成!", "hardware.bluetooth_need_turned_on_error": "藍牙已關閉", "hardware.bluetooth_not_paired_error": "藍牙未配對", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "推薦獎勵", "id.refer_a_friend_desc": "邀請朋友以賺取獎勵", "identical_name_asset_alert": "檢測到錢包中存在相同或相似名稱的資產,請謹慎操作,避免損失。", + "import_backup_password_desc": "請輸入此備份的密碼。", + "import_hardware_phrases_warning": "不要導入硬體錢包的助記詞,請使用連接硬件錢包 ↗", "import_phrase_or_private_key": "匯入助記詞或私鑰", "insufficient_fee_append_desc": "基於最大估計費用:{amount} {symbol}", "interact_with_contract": "互動地址 (至)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "連接您的硬件錢包以訪問 Lightning 帳戶", "ln.authorize_access_network_error": "身份驗證失敗,請檢查網絡連接後重試。", "ln.payment_received_label": "已收到付款", + "log_out_confirmation_text": "您確定要登出 ${email} 嗎?", + "logged_out_feedback": "已成功登出", "login.forgot_passcode": "忘記密碼?", "login.forgot_password": "忘記密碼?", "login.welcome_message": "歡迎回來", @@ -1983,6 +1999,11 @@ "nft.token_address": "代幣地址", "nft.token_id": "代幣 ID", "no_account": "沒有帳戶", + "no_backup_found_google_desc": "請驗證您的 Google Drive 登入,確保您使用正確的 Google 帳戶,或檢查您的網絡連接", + "no_backup_found_icloud_desc": "請驗證你的 iCloud 登入、啟用 iCloud 和鑰匙圈同步,或檢查你的網絡連線", + "no_backup_found_no_wallet": "沒有可備份的錢包", + "no_backup_found_no_wallet_desc": "請先建立錢包", + "no_backups_found": "找不到備份", "no_external_wallet_message": "沒有連接任何外部錢包。連接第三方錢包以在此處查看。", "no_private_key_account_message": "目前沒有私鑰帳戶。添加新帳戶以管理您的資產。", "no_standard_wallet_desc": "仍未有標準錢包", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "您將及時收到帳戶動態、安全提醒及更多重要資訊。", "notifications.test_message_title": "推送通知已準備好!", "notifications.windows_notifications_permission_desc": "從應用程式和其他寄件者取得通知", + "older_backups_description": "這些是舊版本創建的備份。從 {version} 起:錢包需要手動備份,通訊錄已整合到 OneKey Cloud 同步,不再隨雲端備份保存。", "onboarding.activate_device": "啟動您的裝置", "onboarding.activate_device_all_set": "準備就緒", "onboarding.activate_device_by_restore": "恢復錢包", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "儲存圖片", "perps.share_position_title": "分享倉位", "pick_your_device": "選擇你的裝置", + "preparing_backup_desc": "請稍候…", + "preparing_backup_title": "正在準備備份…", "prime.about_cloud_sync": "關於雲端同步", "prime.about_cloud_sync_description": "了解會同步哪些資料,以及我們如何保護你的私隱。", "prime.about_cloud_sync_included_data_title": "同步資料", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "搜索代幣名稱或合約地址", "send_token_selector.select_token": "選擇代幣", "sending_krc20_warning_text": "發送 KRC20 代幣需在硬件確認兩次。請勿取消第二次確認,否則第一次確認發出的 KAS 將難以找回。", + "set_new_backup_password": "設定新備份密碼", + "set_new_backup_password_desc": "新的備份密碼將用於您的下一個備份記錄。現有的備份記錄及其密碼將保持不變。", "setting.floating_icon": "懸浮球", "setting.floating_icon_always_display": "始終顯示", "setting.floating_icon_always_display_description": "啟用後,OneKey圖標會浮動在網頁邊緣,幫助您快速檢查 dApp 的安全信息。", @@ -3080,6 +3106,7 @@ "signature_format_standard": "標準 (Electrum) — 適用於大部分錢包和服務", "signature_format_title": "簽名格式", "signature_type_not_supported_on_model": "設備 {deviceModel} 不支援 {sigType} 簽名格式", + "signed_in_feedback": "登入成功", "skip_firmware_check_dialog_desc": "你確定要跳過檢查嗎?使用最新的 firmware 能為你提供最佳保護。", "skip_firmware_check_dialog_title": "跳過韌體檢查?", "skip_verify_text": "設備不在我身邊", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "在更新過程中,請勿關閉您的應用程式。", "v4_migration.welcome_message": "OneKey 5.0 已到來", "v4_migration.welcome_message_desc": "讓我們安全快速地遷移您的資料。準備好了嗎?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "驗證備份密碼", + "verify_backup_password_desc": "請確認您的雲端備份密碼以繼續。如果您忘記了密碼,可以使用「忘記密碼」來重設。", "verify_message_address_form_description": "支援:{networks}", + "view_older_backups": "查看歷史備份", "wallet.approval_alert_title_summary": "已偵測到 {riskyNumber} 個具風險的授權與 {inactiveNumber} 個未啟用的合約", "wallet.approval_approval_details": "授權詳情", "wallet.approval_approved_token": "已批准的代幣", diff --git a/packages/shared/src/locale/json/zh_TW.json b/packages/shared/src/locale/json/zh_TW.json index d51b126315c0..ccc90ddffdc5 100644 --- a/packages/shared/src/locale/json/zh_TW.json +++ b/packages/shared/src/locale/json/zh_TW.json @@ -158,6 +158,8 @@ "auth.set_passcode": "設置密碼", "auth.set_password": "設定密碼", "auth.with_biometric": "使用 {biometric} 進行身份驗證", + "backing_up_desc": "在備份完成之前,請勿關閉此視窗。", + "backing_up_title": "正在備份…", "backup.address_book_labels": "地址簿 & 標籤", "backup.all_devices": "所有裝置", "backup.backup_deleted": "備份已刪除", @@ -208,6 +210,9 @@ "backup.verify_app_password_to_import_data": "驗證在備份時使用的應用密碼以導入數據", "backup.verify_apple_account_and_icloud_drive_enabled": "請驗證您的 Apple 賬戶登錄,並確保已在 iCloud Drive 中授權給 OneKey 必要的權限", "backup.verify_google_account_and_google_drive_enabled": "請驗證您的 Google 賬戶登錄,並確保已在 Google Drive 中授權給 OneKey 必要的權限", + "backup_restored": "備份已還原!", + "backup_success_toast_title": "備份已完成", + "backup_write_to_cloud_failed": "這次備份失敗了。請再試一次", "balance_detail.button_acknowledge": "確認", "balance_detail.button_balance": "餘額詳情", "balance_detail.button_cancel": "取消", @@ -819,6 +824,7 @@ "earn.withdrawal_take_up_to_number_days": "提出贖回請求後,可能等待 {number} 天", "earn.withdrawal_up_to_number_days": "贖回可能需要多達 {number} 天,然後您的抵押資產將會變得可用", "earn.withdrawn": "已贖回", + "earn_no_assets_deposited": "目前尚未認購任何資產", "earn_reward_distribution_schedule": "獎勵將於下個月 10 號之前發放到您的 Arbitrum 錢包(與以太坊相同的地址)。", "edit_fee_custom_set_as_default_description": "設為 {network} 網路未來交易的預設值", "energy_consumed": "能量消耗", @@ -1031,6 +1037,7 @@ "firmware_update.status_transferring_data": "傳輸資料", "firmware_update.status_validating": "驗證中", "for_reference_only": "僅供參考", + "forgot_password_no_question_mark": "忘記密碼", "form.address_error_invalid": "無效地址", "form.address_placeholder": "地址或域名", "form.amount_placeholder": "輸入金額", @@ -1101,7 +1108,7 @@ "global.allow": "允許", "global.always": "總是", "global.an_error_occurred": "發生錯誤", - "global.an_error_occurred_desc": "我們無法完成您的請求。請在幾分鐘後刷新頁面。", + "global.an_error_occurred_desc": "請重整頁面或稍後再試。", "global.app_wallet": "App 錢包", "global.apply": "應用", "global.approval": "授權", @@ -1111,6 +1118,7 @@ "global.approve": "授權", "global.apr": "APR", "global.asset": "資產", + "global.at_least_variable_characters": "至少 {variable} 個字元", "global.auto": "自動", "global.available": "可用", "global.backed_up": "已備份", @@ -1657,6 +1665,10 @@ "global__multichain": "多鏈", "global_add_money": "儲值", "global_buy_crypto": "購買加密貨幣", + "global_sign_in": "登入", + "google_account_not_signed_in": "Google 帳戶未登入", + "google_play_services_not_available_desc": "請安裝 Google Play 服務並使用您的 Google 帳戶登入。", + "google_play_services_not_available_title": "Google Play 服務無法使用", "hardware.backup_completed": "備份完成!", "hardware.bluetooth_need_turned_on_error": "藍牙已關閉", "hardware.bluetooth_not_paired_error": "藍牙未配對", @@ -1779,6 +1791,8 @@ "id.refer_a_friend": "推薦獎勵", "id.refer_a_friend_desc": "邀請朋友以獲得獎勵", "identical_name_asset_alert": "檢測到錢包中存在相同或相似名稱的資產,請謹慎操作,避免損失。", + "import_backup_password_desc": "請輸入此備份的密碼。", + "import_hardware_phrases_warning": "不要導入硬體錢包的助記詞,請使用連接硬體錢包 ↗", "import_phrase_or_private_key": "匯入助記詞或私鑰", "insufficient_fee_append_desc": "基於最大估計費用:{amount} {symbol}", "interact_with_contract": "互動地址 (至)", @@ -1818,6 +1832,8 @@ "ln.authorize_access_desc": "連接您的硬體錢包以訪問 Lightning 帳戶", "ln.authorize_access_network_error": "驗證失敗,請檢查您的網路連線並再試一次", "ln.payment_received_label": "已收到付款", + "log_out_confirmation_text": "您確定要登出 ${email} 嗎?", + "logged_out_feedback": "已成功登出", "login.forgot_passcode": "忘記密碼?", "login.forgot_password": "忘記密碼?", "login.welcome_message": "歡迎回來", @@ -1983,6 +1999,11 @@ "nft.token_address": "代幣地址", "nft.token_id": "代币 ID", "no_account": "無帳戶", + "no_backup_found_google_desc": "請驗證您的 Google Drive 登入,確認您使用的是正確的 Google 帳戶,或檢查您的網路連線", + "no_backup_found_icloud_desc": "請驗證您的 iCloud 登入、啟用 iCloud 和鑰匙圈同步,或檢查您的網路連線", + "no_backup_found_no_wallet": "沒有可備份的錢包", + "no_backup_found_no_wallet_desc": "請先建立錢包", + "no_backups_found": "找不到備份", "no_external_wallet_message": "沒有連接任何外部錢包。連接第三方錢包以在此處查看。", "no_private_key_account_message": "目前沒有私鑰帳戶。添加新帳戶以管理您的資產。", "no_standard_wallet_desc": "尚無標準錢包", @@ -2017,6 +2038,7 @@ "notifications.test_message_desc": "您將及時收到帳戶動態、安全提醒及更多重要資訊。", "notifications.test_message_title": "推播通知已準備好!", "notifications.windows_notifications_permission_desc": "從應用程式和其他寄件者取得通知", + "older_backups_description": "這些是舊版本創建的備份。從 {version} 起:錢包需要手動備份,通訊錄已整合到 OneKey Cloud 同步,不再隨雲端備份保存。", "onboarding.activate_device": "啟動您的裝置", "onboarding.activate_device_all_set": "準備就緒", "onboarding.activate_device_by_restore": "恢復錢包", @@ -2368,6 +2390,8 @@ "perps.share_position_btn_save_img": "儲存圖片", "perps.share_position_title": "分享倉位", "pick_your_device": "選擇您的裝置", + "preparing_backup_desc": "請稍候…", + "preparing_backup_title": "正在準備備份…", "prime.about_cloud_sync": "關於雲端同步", "prime.about_cloud_sync_description": "了解會同步哪些資料,以及我們如何保護您的隱私。", "prime.about_cloud_sync_included_data_title": "同步資料", @@ -2889,6 +2913,8 @@ "send_token_selector.search_placeholder": "搜尋代幣名稱或合約地址", "send_token_selector.select_token": "選擇代幣", "sending_krc20_warning_text": "發送 KRC20 代幣需在硬件確認兩次。請勿取消第二次確認,否則第一次確認發出的 KAS 將難以找回。", + "set_new_backup_password": "設定新的備份密碼", + "set_new_backup_password_desc": "新的備份密碼將用於您的下一個備份記錄。現有的備份記錄及其密碼將保持不變。", "setting.floating_icon": "懸浮球", "setting.floating_icon_always_display": "始終顯示", "setting.floating_icon_always_display_description": "啟用後,OneKey 圖標會浮動在網頁邊緣,幫助您快速檢查 dApp 的安全資訊。", @@ -3080,6 +3106,7 @@ "signature_format_standard": "標準 (Electrum) — 適用於大多數錢包和服務", "signature_format_title": "簽名格式", "signature_type_not_supported_on_model": "裝置 {deviceModel} 不支援 {sigType} 簽名格式", + "signed_in_feedback": "登入成功", "skip_firmware_check_dialog_desc": "確定要跳過檢查嗎?使用最新版韌體能為您提供最佳保護。", "skip_firmware_check_dialog_title": "略過�韌體檢查?", "skip_verify_text": "裝置不在我身邊", @@ -3280,7 +3307,7 @@ "swap_page.toast.swap_failed_detail": "{number} {tokenSymbol} → {number} {tokenSymbol}", "swap_page.toast.swap_successful": "交換成功", "swap_page.toast.swap_successful_detail": "{number} {symbol} → {number} {symbol}", - "swap_page.toast.taproot_unsupported": "ThorSwap 目前不支援 Taproot 格式的 BTC 地址。", + "swap_page.toast.taproot_unsupported": "ThorSwap 目前不支援 Taproot 格式的 BTC 地址", "swap_page.toast.token_not_supported": "不支援代幣", "swap_page.usd_value": "${number}", "swap_process.build_and_estimate_tx": "建立並估算交易", @@ -3613,8 +3640,10 @@ "v4_migration.update_in_progress_alert_title": "更新時,請不要關閉您的應用程式", "v4_migration.welcome_message": "OneKey 5.0 已經來了!", "v4_migration.welcome_message_desc": "讓我們安全快速地遷移您的資料。準備好了嗎?", - "verify_backup_password": "Verify backup password", + "verify_backup_password": "驗證備份密碼", + "verify_backup_password_desc": "請確認您的雲端備份密碼以繼續。如果您忘記密碼,可以使用「忘記密碼」來重設。", "verify_message_address_form_description": "支援:{networks}", + "view_older_backups": "查看歷史備份", "wallet.approval_alert_title_summary": "已偵測到 {riskyNumber} 個具風險的授權與 {inactiveNumber} 個未啟用的合約", "wallet.approval_approval_details": "授權詳情", "wallet.approval_approved_token": "已核准的代幣", diff --git a/packages/shared/src/routes/staking.ts b/packages/shared/src/routes/staking.ts index cfcd7f9afa51..0ca5518765e1 100644 --- a/packages/shared/src/routes/staking.ts +++ b/packages/shared/src/routes/staking.ts @@ -9,6 +9,7 @@ export enum EModalStakingRoutes { InvestmentDetails = 'InvestmentDetails', Stake = 'Stake', Withdraw = 'Withdraw', + ManagePosition = 'ManagePosition', Claim = 'Claim', ProtocolDetails = 'ProtocolDetails', ProtocolDetailsV2 = 'ProtocolDetailsV2', @@ -29,6 +30,8 @@ type IBaseRouteParams = { interface IDetailPageInfoParams extends IBaseRouteParams { protocolInfo?: IProtocolInfo; tokenInfo?: IEarnTokenInfo; + symbol?: string; + provider?: string; } export type IModalStakingParamList = { [EModalStakingRoutes.InvestmentDetails]: undefined; @@ -52,6 +55,14 @@ export type IModalStakingParamList = { details?: IStakeProtocolDetails; // note: does not contain accountId, etc. account information }; + [EModalStakingRoutes.ManagePosition]: { + networkId: string; + symbol: string; + provider: string; + details?: IStakeProtocolDetails; + vault?: string; + tab?: 'deposit' | 'withdraw'; + }; [EModalStakingRoutes.Stake]: IDetailPageInfoParams & { currentAllowance: string; onSuccess?: () => void; diff --git a/packages/shared/src/routes/tabDiscovery.ts b/packages/shared/src/routes/tabDiscovery.ts index 555192f85885..d56af2e7f5c5 100644 --- a/packages/shared/src/routes/tabDiscovery.ts +++ b/packages/shared/src/routes/tabDiscovery.ts @@ -7,5 +7,6 @@ export enum ETabDiscoveryRoutes { export type ITabDiscoveryParamList = { [ETabDiscoveryRoutes.TabDiscovery]: { defaultTab?: ETranslations; + earnTab?: 'assets' | 'portfolio' | 'faqs'; }; }; diff --git a/packages/shared/src/routes/tabEarn.ts b/packages/shared/src/routes/tabEarn.ts index f2d7657f3d01..a28444a75659 100644 --- a/packages/shared/src/routes/tabEarn.ts +++ b/packages/shared/src/routes/tabEarn.ts @@ -1,7 +1,33 @@ export enum ETabEarnRoutes { EarnHome = 'EarnHome', + EarnProtocols = 'EarnProtocols', + EarnProtocolDetails = 'EarnProtocolDetails', + EarnProtocolDetailsShare = 'EarnProtocolDetailsShare', } export type ITabEarnParamList = { - [ETabEarnRoutes.EarnHome]: undefined; + [ETabEarnRoutes.EarnHome]: + | undefined + | { + tab?: 'assets' | 'portfolio' | 'faqs'; + }; + [ETabEarnRoutes.EarnProtocols]: { + symbol: string; + filterNetworkId?: string; + logoURI?: string; + }; + [ETabEarnRoutes.EarnProtocolDetails]: { + networkId: string; + accountId: string; + indexedAccountId?: string; + symbol: string; + provider: string; + vault?: string; + }; + [ETabEarnRoutes.EarnProtocolDetailsShare]: { + network: string; // network name, like 'ethereum', 'bitcoin' + symbol: string; + provider: string; + vault?: string; + }; }; diff --git a/packages/shared/src/utils/earnUtils.ts b/packages/shared/src/utils/earnUtils.ts index dfd45132c51d..a030439da6c0 100644 --- a/packages/shared/src/utils/earnUtils.ts +++ b/packages/shared/src/utils/earnUtils.ts @@ -1,6 +1,8 @@ import { EEarnProviderEnum } from '../../types/earn'; import type { IEarnPermitCacheKey } from '../../types/earn'; +import type { IEarnToken } from '../../types/staking'; +import type { IToken } from '../../types/token'; function getEarnProviderEnumKey( providerString: string, @@ -85,6 +87,22 @@ function isUSDTonETHNetwork({ return networkId === 'evm--1' && symbol === 'USDT'; } +function convertEarnTokenToIToken(earnToken?: IEarnToken): IToken | undefined { + if (!earnToken?.address) { + return undefined; + } + + return { + address: earnToken.address, + decimals: earnToken.decimals, + isNative: earnToken.isNative, + logoURI: earnToken.logoURI, + name: earnToken.name, + symbol: earnToken.symbol, + uniqueKey: earnToken.uniqueKey, + }; +} + export default { getEarnProviderEnumKey, isMorphoProvider, @@ -101,4 +119,5 @@ export default { isUSDTonETHNetwork, isVaultBasedProvider, isValidatorProvider, + convertEarnTokenToIToken, }; diff --git a/packages/shared/src/utils/routeUtils.ts b/packages/shared/src/utils/routeUtils.ts index 914af629554f..41c17db54d26 100644 --- a/packages/shared/src/utils/routeUtils.ts +++ b/packages/shared/src/utils/routeUtils.ts @@ -176,6 +176,11 @@ export const buildAllowList = ( showUrl: true, showParams: true, }, + [pagePath`${ERootRoutes.Modal}${EModalRoutes.StakingModal}${EModalStakingRoutes.ManagePosition}`]: + { + showUrl: true, + showParams: true, + }, // Page: /main/tab-Swap/TabSwap // Don't worry, the URL here is virtual, actually /swap. // it will automatically find the real route according to the route stacks. diff --git a/packages/shared/types/staking.ts b/packages/shared/types/staking.ts index 56b7a30db25e..6a8c491ba030 100644 --- a/packages/shared/types/staking.ts +++ b/packages/shared/types/staking.ts @@ -8,6 +8,7 @@ import type { import type { IDialogProps } from '@onekeyhq/components/src/composite/Dialog/type'; import type { INetworkAccount } from './account'; +import type { IDiscoveryBanner } from './discovery'; import type { IEarnAvailableAssetAprInfo } from './earn'; import type { IFetchTokenDetailItem, IToken } from './token'; import type { ESpotlightTour } from '../src/spotlight'; @@ -363,6 +364,7 @@ export type IProtocolInfo = { minUnstakeAmount?: string; claimable?: string; remainingCap?: string; + withdrawAction?: IEarnWithdrawActionIcon; }; export interface IEarnToken { @@ -534,6 +536,7 @@ export enum EClaimType { Claim = 'claim', ClaimOrder = 'claimOrder', ClaimWithKyc = 'claimWithKyc', + ClaimAirdrop = 'claimAirdrop', } export interface IEarnClaimActionIcon { @@ -624,6 +627,10 @@ interface IEarnGridItem { description: IEarnText; button?: IEarnActionIcon; tooltip?: IEarnTooltip; + items?: { + title: IEarnText; + logoURI: string; + }[]; type?: 'default' | 'info'; } @@ -687,16 +694,67 @@ export interface IEarnWithdrawOrderActionIcon { type: EStakingActionType; disabled: boolean; text: IEarnText; - data: { + data?: { text: IEarnText; }; } +export interface IEarnDepositActionData { + type: 'deposit'; + disabled: boolean; + text: IEarnText; + data: { + balance: string; + token: { + info: IEarnToken; + price: string; + }; + }; +} + +export interface IEarnWithdrawActionData { + type: 'withdraw' | 'withdrawOrder'; + disabled: boolean; + text: IEarnText; + data?: { + balance?: string; + token?: { + info: IEarnToken; + price: string; + }; + text?: IEarnText; + }; +} + +export interface IEarnManagePageResponse { + deposit?: IEarnDepositActionData; + withdraw?: IEarnWithdrawActionData; + receive?: IEarnReceiveActionIcon; + trade?: IEarnTradeActionIcon; + history?: IEarnHistoryActionIcon; + approve?: { + allowance: string; + approveType: string; + approveTarget: string; + }; + nums?: { + overflow?: string; + minUnstakeAmount?: string; + maxUnstakeAmount?: string; + minTransactionFee?: string; + claimable?: string; + remainingCap?: string; + }; + alertsStake?: IEarnAlert[]; + alertsWithdraw?: IEarnAlert[]; +} + export type IEarnDetailActions = | IEarnDepositActionIcon | IEarnWithdrawActionIcon | IEarnHistoryActionIcon | IEarnWithdrawOrderActionIcon + | IEarnClaimWithKycActionIcon | IEarnActivateActionIcon; export interface IEarnAlert { @@ -721,12 +779,6 @@ export interface IStakeEarnDetail { icon: IEarnIcon; }[]; }; - apyDetail?: { - type: 'default'; - title: IEarnText; - description: IEarnText; - button: IEarnActionIcon; - }; actions?: IEarnDetailActions[]; subscriptionValue?: ISubscriptionValue; tags?: IStakeBadgeTag[]; @@ -736,6 +788,28 @@ export interface IStakeEarnDetail { startTime: number; endTime: number; }; + apyDetail?: { + type: 'default'; + token: { + info: IEarnToken; + price: string; + }; + fiatValue: string; + formattedValue: string; + title: IEarnText; + description?: IEarnText; + badge: IEarnBadge; + tooltip?: IEarnTooltip; + button?: IEarnActionIcon; + }; + intro?: { + title: IEarnText; + items: IEarnGridItem[]; + }; + performance?: { + title: IEarnText; + items: IEarnGridItem[]; + }; portfolios?: { title: IEarnText; items: { @@ -898,6 +972,7 @@ export type IStakeProtocolListItem = { }; isEarning: boolean; aprInfo?: IEarnAvailableAssetAprInfo; + tvl?: IEarnText; }; export type IRewardApys = { @@ -1039,6 +1114,8 @@ export type IRecommendAsset = { export interface IEarnAtomData { earnAccount?: Record; availableAssetsByType?: Record; + recommendedTokens?: IRecommendAsset[]; + banners?: IDiscoveryBanner[]; refreshTrigger?: number; } @@ -1080,13 +1157,127 @@ export interface IInvestment { rewardNum?: IEarnRewardNum; rewards?: string; vault?: string; + vaultName?: string; + networkInfo?: { + logoURI: string; + }; } + export interface IEarnInvestmentItem { name: string; logoURI: string; investment: IInvestment[]; } +export interface IEarnInvestmentItemV2 { + totalFiatValue: string; + protocol: { + vault?: string; + vaultName?: string; + providerDetail: { + code: string; + name: string; + logoURI: string; + }; + }; + assets: { + token: { + info: { + symbol: string; + logoURI: string; + }; + }; + deposit: { + title: IEarnText; + description: IEarnText; + }; + earnings24h: { + title: IEarnText; + }; + rewardAssets: { + title: IEarnText; + tooltip: IEarnTooltip; + button: + | IEarnClaimActionIcon + | IEarnClaimWithKycActionIcon + | IEarnListaCheckActionIcon; + description: IEarnText; + }[]; + assetsStatus: { + title: IEarnText; + description: IEarnText; + tooltip: IEarnTooltip; + }[]; + buttons: { + type: string; + text: { + text: string; + }; + disabled: boolean; + }[]; + }[]; + network: { + networkId: string; + name: string; + logoURI: string; + }; +} + +export interface IEarnAirdropInvestmentItemV2 { + totalFiatValue: string; + protocol: { + vault?: string; + vaultName?: string; + providerDetail: { + code: string; + name: string; + logoURI: string; + }; + }; + assets: { + token: { + info: { + address?: string; + symbol: string; + logoURI: string; + }; + }; + airdropAssets: { + title: IEarnText; + tooltip: IEarnTooltip; + button: IEarnClaimActionIcon; + description: IEarnText; + }[]; + }[]; + network: { + networkId: string; + name: string; + logoURI: string; + }; +} + +export type IEarnPortfolioAsset = IEarnInvestmentItemV2['assets'][number] & { + // Metadata containing protocol and network information for this asset + metadata: { + protocol: IEarnInvestmentItemV2['protocol']; + network: IEarnInvestmentItemV2['network']; + }; +}; + +export type IEarnPortfolioAirdropAsset = + IEarnAirdropInvestmentItemV2['assets'][number] & { + // Metadata containing protocol and network information for this airdrop asset + metadata: { + protocol: IEarnAirdropInvestmentItemV2['protocol']; + network: IEarnAirdropInvestmentItemV2['network']; + }; + }; + +export type IEarnPortfolioInvestment = Omit & { + assets: IEarnPortfolioAsset[]; // Only normal type assets + airdropAssets: IEarnPortfolioAirdropAsset[]; // Only airdrop type assets +}; + export interface IEarnFAQListItem { question: string; answer: string; @@ -1200,6 +1391,21 @@ export interface IEarnSummary { }[]; } +export interface IEarnSummaryV2 { + title: IEarnText; + description: IEarnText; + distributed: { + title: IEarnText; + token: IEarnToken; + button: IEarnHistoryActionIcon; + }[]; + undistributed: { + title: IEarnText; + description: IEarnText; + token: IEarnToken; + }[]; +} + export type IStakeBlockRegionResponse = | { isBlockedRegion: true; @@ -1220,3 +1426,14 @@ export type IStakeBlockRegionResponse = isBlockedRegion: false; countryCode: string; }; + +export interface IApyHistoryItem { + apy: string; + timestamp: number; +} + +export interface IApyHistoryResponse { + code: number; + message: string; + data: IApyHistoryItem[]; +}