Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release-app-bundles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ jobs:
checkout_ref: ${{ needs.prepare-params.outputs.commit }}
secrets:
COVALENT_KEY: ${{ secrets.COVALENT_KEY }}
JPUSH_KEY: ${{ secrets.JPUSH_KEY }}
SENTRY_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_DSN_REACT_NATIVE: ${{ secrets.SENTRY_DSN_REACT_NATIVE }}
SENTRY_DSN_WEB: ${{ secrets.SENTRY_DSN_WEB }}
Expand Down Expand Up @@ -321,6 +322,7 @@ jobs:
checkout_ref: ${{ needs.prepare-params.outputs.commit }}
secrets:
COVALENT_KEY: ${{ secrets.COVALENT_KEY }}
JPUSH_KEY: ${{ secrets.JPUSH_KEY }}
SENTRY_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_DSN_REACT_NATIVE: ${{ secrets.SENTRY_DSN_REACT_NATIVE }}
SENTRY_DSN_WEB: ${{ secrets.SENTRY_DSN_WEB }}
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/release-native-bundle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ on:
secrets:
COVALENT_KEY:
required: false
JPUSH_KEY:
required: false
SENTRY_TOKEN:
required: false
SENTRY_DSN_REACT_NATIVE:
Expand Down Expand Up @@ -370,13 +372,19 @@ jobs:
- name: Build Bundle
env:
NODE_OPTIONS: '--max_old_space_size=8192'
JPUSH_KEY: ${{ secrets.JPUSH_KEY }}
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
ASC_PROVIDER: ${{ secrets.ASC_PROVIDER }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
UNION_BUILD: 'true'
ENABLE_NATIVE_BACKGROUND_THREAD: 'true'
run: 'yarn app:build-bundle:${{ matrix.platform }}'
run: |
if [ -z "$JPUSH_KEY" ]; then
echo "::error::JPUSH_KEY is required to build native bundles"
exit 1
fi
yarn app:build-bundle:${{ matrix.platform }}

- name: Validate split-bundle integrity
# Hard gate: fails the job if any segment ships un-rewritten Metro
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ Cases are appended by AI after each bug fix. Do NOT reorder or delete entries
**Root Cause**: All Perps caches live in one `simple_db_v5:perp` record. Chromium stores large IndexedDB values as external blob files; a crash corrupted the blob so every read rejected with `UnknownError: Failed to read large IndexedDB value`. `setRawData(builder)` reads the old record before writing, so all writes failed too — the record could never be repaired by normal usage.
**Fix**: Opt-in self-heal in `SimpleDbEntityBase` (perp only): on the exact corruption signature, retry once, then remove the record with write-overlap vetoes (writeSeq + pendingWrites snapshot); read generation prevents in-flight reads from resurrecting cleared/overwritten cache; Settings → Clear cache gained a "Perps" item backed by a runtime clear epoch in ServiceWebviewPerp.
**Catchable by**: NEW — storage-layer read errors need a recovery path for caches that can be rebuilt; read-before-write persistence cannot self-repair a corrupted record

## Case: Perps banner/push cold start opens the previous market
**Date**: 2026-08-18 | **Platforms**: iOS, Android, desktop, extension (every runtime with a context-less Perps entry)
**Symptom**: Killing the app, then tapping a Perps banner or push, landed on the Perps tab still showing the market from the previous session; a second tap worked. Reported on 6.5.0 for `para:*` markets, but reproducible with any symbol and from the tray, universal search, the Home perps card and the market list (OK-60543).
**Root Cause**: #12680 gave `preferredInstrument` unconditional precedence in `buildInitialTradeInstrumentSwitchParams`, on the premise that the UI writes the instrument synchronously at the start of a switch while the background atoms are written at the end — so the UI copy is never the staler record. That holds within a session. On a cold start `activeTradeInstrumentAtom` is hydrated from its `coldStartCache`, i.e. the *previous* session, while a context-less caller has just written the tapped market to the background atoms. The transient `PerpSwitchActiveInstrument` listener does not exist yet either, so the event that would have corrected it was dropped.
**Fix**: `ServiceHyperliquid` keeps a one-shot pending instrument, recorded by each context-less caller before the navigation that mounts the tab and ignored once the initial-symbol latch is taken; the claiming run uses it in place of the restored instrument. `buildInitialTradeInstrumentSwitchParams` is unchanged, so the restore path is untouched.
**Catchable by**: NEW — a value restored from a cold-start cache is not a "recent write"; precedence rules that rank two copies of the same state must say which side a fresh cold start makes authoritative, and a transient event bus cannot carry intent across a mount boundary
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ISubmenuColumnProps {
}

const dragRegionStyle = { WebkitAppRegion: 'drag' } as any;
const expandedBackgroundStyle = { transition: 'opacity 150ms ease' };

export function SubmenuColumn({
webPageTabBar,
Expand All @@ -27,7 +28,7 @@ export function SubmenuColumn({
boxShadow: isExpanded ? '10px 0 30px -10px rgba(0, 0, 0, 0.10)' : 'none',
willChange: isExpanded ? ('width' as const) : ('auto' as const),
transition:
'background-color 150ms ease, border-color 150ms ease, border-radius 150ms ease, box-shadow 150ms ease',
'border-color 150ms ease, border-radius 150ms ease, box-shadow 150ms ease',
}),
[isExpanded],
);
Expand All @@ -53,7 +54,6 @@ export function SubmenuColumn({
left={0}
bottom={0}
width={isExpanded ? EXPANDED_SUBMENU_WIDTH : COLLAPSED_SUBMENU_WIDTH}
bg={isExpanded ? '$bgApp' : '$bgSidebar'}
pt={8}
px="$3"
zIndex={10}
Expand All @@ -68,6 +68,27 @@ export function SubmenuColumn({
overflow="hidden"
style={expandedStyle}
>
{/* Static token layers follow theme CSS variables without waiting for this animated subtree to rerender. */}
<Stack
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
bg="$bgSidebar"
pointerEvents="none"
/>
<Stack
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
bg="$bgApp"
opacity={isExpanded ? 1 : 0}
pointerEvents="none"
style={expandedBackgroundStyle}
/>
{webPageTabBar}
</YStack>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
XStack,
YStack,
} from '@onekeyhq/components/src/primitives';
import { useTheme } from '@onekeyhq/components/src/shared/tamagui';
import { ANIMATE_ONLY_OPACITY_TRANSFORM } from '@onekeyhq/components/src/utils/animationConstants';
import { MIN_SIDEBAR_WIDTH } from '@onekeyhq/components/src/utils/sidebar';
import { appEventBus } from '@onekeyhq/shared/src/eventBus/appEventBus';
Expand Down Expand Up @@ -644,7 +643,6 @@ export function DesktopLeftSideBar({
}) {
const { routes } = state;
const { top } = useSafeAreaInsets(); // used for ipad
const theme = useTheme();
const handleTabPress = useTabAction(navigation);

const isShowWebTabBar = platformEnv.isDesktop || platformEnv.isNativeIOS;
Expand Down Expand Up @@ -722,15 +720,6 @@ export function DesktopLeftSideBar({
? isRouteActive(deviceRoute, focusedRouteName, extraConfig?.name)
: false;

const containerStyle = useMemo(
() => ({
backgroundColor: theme.bgSidebar.val,
paddingTop: top,
zIndex: 2,
}),
[theme.bgSidebar.val, top],
);

const handleDevicePress = useCallback(() => {
if (!deviceRoute) return;
handleTabPress(deviceRoute, isDeviceActive);
Expand All @@ -743,7 +732,12 @@ export function DesktopLeftSideBar({
}, [deviceRoute, isDeviceActive, handleTabPress, descriptors]);

return (
<XStack testID="Desktop-AppSideBar-Container" style={containerStyle}>
<XStack
testID="Desktop-AppSideBar-Container"
bg="$bgSidebar"
pt={top}
zIndex={2}
>
<YStack w={MIN_SIDEBAR_WIDTH}>
{/* eslint-disable no-nested-ternary */}
{platformEnv.isDesktopMac ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,13 @@ export default class ServiceHyperliquid extends ServiceBase {
// on every modal push.
private _initialSymbolSelectClaimed = false;

// A context-less caller picks the market before the Perp page mounts, so the
// switch event it emits has no listener yet and the cold-start restore would
// replay the previous session's instrument over the user's choice.
private _pendingInitialTradeInstrument:
| { coin: string; mode: ITradingMode }
| undefined;

constructor({ backgroundApi }: { backgroundApi: any }) {
super({ backgroundApi });
void this.init();
Expand All @@ -566,6 +573,62 @@ export default class ServiceHyperliquid extends ServiceBase {
return true;
}

// Ignored once the latch is taken: from then on the Perp page is live and
// switches through the event bus, so a value recorded here could only
// override a market the user picked afterwards. That makes "first mount
// only" a property of the store rather than a rule every caller upholds.
@backgroundMethod()
async setPendingInitialTradeInstrument(params: {
coin: string;
mode: ITradingMode;
}): Promise<void> {
if (!params.coin || this._initialSymbolSelectClaimed) {
return;
}
this._pendingInitialTradeInstrument = {
coin: params.coin,
mode: params.mode,
};
}

// Three sequential proxy hops used to sit between the first Perp frame and
// the symbol it should show. Each is cheap when the background is idle and
// ~220ms when it is not, which is exactly the cold start the user waits on.
// The universe read stays behind `claimed` so a non-claiming run does no more
// work than before.
@backgroundMethod()
async prepareInitialSymbolSelect(): Promise<{
claimed: boolean;
pendingInitialTradeInstrument:
| { coin: string; mode: ITradingMode }
| undefined;
instrumentTarget: Awaited<
ReturnType<ServiceHyperliquid['getActiveTradeInstrumentTarget']>
>;
tradingUniverse:
| Awaited<ReturnType<ServiceHyperliquid['getTradingUniverse']>>
| undefined;
}> {
const claimed = await this.tryClaimInitialSymbolSelect();
// Taking it here rather than in the setter keeps the two halves of "first
// mount only" next to each other; a call that lost the race is already
// a no-op by the line above.
const pendingInitialTradeInstrument = claimed
? this._pendingInitialTradeInstrument
: undefined;
this._pendingInitialTradeInstrument = undefined;
const instrumentTarget = await this.getActiveTradeInstrumentTarget();
const tradingUniverse = claimed
? await this.getTradingUniverse()
: undefined;
return {
claimed,
pendingInitialTradeInstrument,
instrumentTarget,
tradingUniverse,
};
}

private get exchangeService(): ServiceHyperliquidExchange {
return this.backgroundApi.serviceHyperliquidExchange;
}
Expand Down
31 changes: 31 additions & 0 deletions packages/kit/src/components/HyperlinkText/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,37 @@ export function HyperlinkText({
{string}
</SizableText>
),
// Semantic color tags. Deliberately no per-color
// `*TextProps` escape hatch: five more ISizableTextProps would
// bloat an already-wide prop type and drag five more entries
// into the memo deps, while `<text>` + textProps already covers
// a caller that needs an arbitrary color. The tokens are
// theme-aware, so these follow light/dark on their own.
red: ([string]) => (
<SizableText {...basicTextProps} color="$textCritical">
{string}
</SizableText>
),
green: ([string]) => (
<SizableText {...basicTextProps} color="$textSuccess">
{string}
</SizableText>
),
yellow: ([string]) => (
<SizableText {...basicTextProps} color="$textCaution">
{string}
</SizableText>
),
blue: ([string]) => (
<SizableText {...basicTextProps} color="$textInfo">
{string}
</SizableText>
),
grey: ([string]) => (
<SizableText {...basicTextProps} color="$textSubdued">
{string}
</SizableText>
),
text: (chunks) => (
<>
{chunks.map((chunk, index) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import type { IHex } from '@onekeyhq/shared/types/hyperliquid/sdk';
import { useNetworkRestore } from '../../../hooks/useNetworkRestore';
import { useThemeVariant } from '../../../hooks/useThemeVariant';
import WebView from '../../WebView';
import { useNavigationHandler, useTradingViewUrl } from '../hooks';
import {
syncTradingViewTheme,
useNavigationHandler,
useTradingViewUrl,
} from '../hooks';

import { MESSAGE_TYPES } from './constants/messageTypes';
import { useChartLines, useTradeUpdates } from './hooks';
Expand Down Expand Up @@ -285,6 +289,10 @@ export function TradingViewPerpsV2(
const [, setMounted] = usePerpsCandlesWebviewMountedAtom();
const webRef = useRef<IWebViewRef | null>(null);
const theme = useThemeVariant();
const latestThemeRef = useRef(theme);
latestThemeRef.current = theme;
const onLoadEndRef = useRef(onLoadEnd);
onLoadEndRef.current = onLoadEnd;
const themeColors = useTheme();
const tradingViewBackgroundColor = themeColors.bgApp.val;
const actions = useHyperliquidActions();
Expand All @@ -301,7 +309,9 @@ export function TradingViewPerpsV2(
? activeTradeInstrument.universe?.baseSzDecimals
: activeTradeInstrument.universe?.szDecimals;
const _webviewKey = useMemo(() => {
return `${theme}-${webviewKey || ''}${
const themeKey =
platformEnv.isDesktop || platformEnv.isNative ? '' : `${theme}-`;
return `${themeKey}${webviewKey || ''}${
reloadOnSymbolChange ? `-${symbol}` : ''
}`;
}, [reloadOnSymbolChange, symbol, theme, webviewKey]);
Expand Down Expand Up @@ -379,6 +389,7 @@ export function TradingViewPerpsV2(

const { finalUrl: staticTradingViewUrl } = useTradingViewUrl({
additionalParams,
theme,
});
const isSpotDisplayNameSyncRequired =
reloadOnSymbolChange && (!!displayPair || !!displayCoin);
Expand Down Expand Up @@ -480,12 +491,14 @@ export function TradingViewPerpsV2(
}, [restoreNonce]);

const onChartLinesReady = useCallback(() => {
syncTradingViewTheme(webRef.current, latestThemeRef.current);
hasPerpsReadyRef.current = true;
setChartContentReadyWebviewKey(_webviewKey);
setChartLinesReadyWebviewKey(_webviewKey);
}, [_webviewKey]);

const onChartReady = useCallback(() => {
syncTradingViewTheme(webRef.current, latestThemeRef.current);
setChartContentReadyWebviewKey(_webviewKey);
}, [_webviewKey]);

Expand Down Expand Up @@ -638,6 +651,15 @@ export function TradingViewPerpsV2(
webRef.current = ref;
}, []);

useEffect(() => {
syncTradingViewTheme(webRef.current, theme);
}, [theme]);

const handleLoadEnd = useCallback(() => {
syncTradingViewTheme(webRef.current, latestThemeRef.current);
onLoadEndRef.current?.();
}, []);

const onShouldStartLoadWithRequest = useCallback(
(event: WebViewNavigation) => handleNavigation(event),
[handleNavigation],
Expand All @@ -655,7 +677,7 @@ export function TradingViewPerpsV2(
customReceiveHandler={customReceiveHandler}
skipBackgroundBridge
onWebViewRef={onWebViewRef}
onLoadEnd={onLoadEnd}
onLoadEnd={handleLoadEnd}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
nativeInjectedJavaScriptBeforeContentLoaded={
platformEnv.isNativeAndroid
Expand Down
Loading
Loading