Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7eadc96
feat: add Infini subscription deep link OK-61658
cursoragent Sep 4, 2026
aecdf22
refactor: simplify Infini subscription deep link
cursoragent Sep 4, 2026
af57f07
fix: gate Infini deep link login and refresh toast
cursoragent Sep 4, 2026
8d3fc46
refactor: inline Infini deep link login and toast helpers
cursoragent Sep 5, 2026
44e18bb
feat: open Infini subscription from web landing in extension
cursoragent Sep 5, 2026
d2dc08c
fix: simplify Infini subscription return flow
yikZero Sep 5, 2026
75fea34
chore: merge upstream x for Infini subscription links
yikZero Sep 5, 2026
07385d3
refactor: share deep link landing presentation
yikZero Sep 5, 2026
dd6c348
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 5, 2026
1ced282
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 5, 2026
e44bd27
fix: serialize Prime subscription login and client handoff
yikZero Sep 5, 2026
7a9a002
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 6, 2026
f375b8b
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
53adcc0
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
203be90
fix: serialize Infini login and resolve route conflicts
yikZero Sep 7, 2026
4aa6947
chore: retain remote Infini branch sync history
yikZero Sep 7, 2026
3c90933
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
9546032
fix: preserve Infini permissions with travel mode ingress
yikZero Sep 7, 2026
ca95bc9
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
6e01f3f
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
3fbd2cc
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
499c38e
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
c0b2465
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 7, 2026
9e9f8ae
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
54a7e0c
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
0ef6d53
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
c907151
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
2f3c8ff
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
cf62c48
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
23a0572
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
afbd86b
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
2ce2b31
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 8, 2026
153ea6f
fix: sync upstream translation enum repair
yikZero Sep 8, 2026
7c04504
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 9, 2026
d27caf2
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 9, 2026
80a525b
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 9, 2026
9bdca98
Merge branch 'x' into yikzero/prime-infini-deeplink-22fe
yikZero Sep 9, 2026
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
5 changes: 5 additions & 0 deletions apps/mobile/bundle-registry/module-id-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -20354,9 +20354,11 @@
"packages/kit/src/routes/Tab/router.ts": 7930,
"packages/kit/src/routes/WebView/Navigator.tsx": 1529,
"packages/kit/src/routes/WebView/router.tsx": 2973,
"packages/kit/src/routes/config/deeplink/ExtPrimeSubscriptionHandoffOnMount.tsx": 8565,
"packages/kit/src/routes/config/deeplink/handler.native.ts": 5162,
"packages/kit/src/routes/config/deeplink/index.ts": 19677,
"packages/kit/src/routes/config/deeplink/parseWebViewDeepLink.ts": 24518,
"packages/kit/src/routes/config/deeplink/primeSubscriptionExtHandoff.ts": 14420,
"packages/kit/src/routes/config/deeplink/referralLandingLink.ts": 11494,
"packages/kit/src/routes/config/deeplink/referralLandingOverlayGuard.tsx": 19410,
"packages/kit/src/routes/config/deeplink/referralLandingRequestGuard.ts": 22849,
Expand Down Expand Up @@ -21337,6 +21339,7 @@
"packages/kit/src/views/Home/components/DeFiListBlock/index.ts": 6453,
"packages/kit/src/views/Home/components/DeFiListBlock/overviewColsResolver.ts": 746,
"packages/kit/src/views/Home/components/DeFiListBlock/useIsDeFiEnabled.ts": 20241,
"packages/kit/src/views/Home/components/DeepLinkLanding.tsx": 6738,
"packages/kit/src/views/Home/components/EarnListView/EarnListView.tsx": 10128,
"packages/kit/src/views/Home/components/EarnListView/index.ts": 20912,
"packages/kit/src/views/Home/components/HomeStickyHeaderContext.tsx": 17460,
Expand Down Expand Up @@ -21435,6 +21438,8 @@
"packages/kit/src/views/Home/pages/nativeHomePortfolioViewModel.ts": 14120,
"packages/kit/src/views/Home/pages/nativeHomeRendererDecision.ts": 1668,
"packages/kit/src/views/Home/pages/nativeHomeSpotMarketViewModel.ts": 5745,
"packages/kit/src/views/Home/pages/primeSubscription/PrimeSubscriptionLandingPage.tsx": 13314,
"packages/kit/src/views/Home/pages/primeSubscription/openPrimeSubscriptionFromWebLanding.ts": 4342,
"packages/kit/src/views/Home/pages/redeemBitcoinVoucher/RedeemBitcoinVoucherLandingPage.tsx": 9891,
"packages/kit/src/views/Home/pages/referralLanding/ReferralLandingPage.tsx": 7264,
"packages/kit/src/views/Home/pages/referralLanding/components/ReferralWebLanding.tsx": 17381,
Expand Down
16 changes: 5 additions & 11 deletions packages/kit-bg/src/apis/BackgroundApiBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,8 @@ import { jotaiInit } from '../states/jotai/jotaiInit';
import {
isBackgroundApiAtomWritable,
isExtensionInternalCall,
isProviderApiPrivateAllowedKeylessOrigin,
isProviderApiPrivateAllowedMethod,
isProviderApiPrivateAllowedOrigin,
isProviderApiPrivateKeylessMethod,
isProviderApiPrivateOriginDenied,
} from './backgroundApiPermissions';
import { travelModeCommandDispatcher } from './TravelModeCommandDispatcher';
import { travelModeDappRequestIngress } from './TravelModeDappRequestIngress';
Expand Down Expand Up @@ -1007,19 +1005,15 @@ class BackgroundApiBase implements IBackgroundApiBridge {
operation: async () => {
const { scope, origin } = payload;
const payloadData = payload?.data as IJsonRpcRequest;
const isKeylessPrivateMethod = isProviderApiPrivateKeylessMethod(
payloadData?.method,
);
const provider: ProviderApiBase = await this.getProviderApi(
scope as IInjectedProviderNames,
);
if (
scope === IInjectedProviderNames.$private &&
((isKeylessPrivateMethod &&
!isProviderApiPrivateAllowedKeylessOrigin(origin)) ||
(!isKeylessPrivateMethod &&
!isProviderApiPrivateAllowedOrigin(origin) &&
!isProviderApiPrivateAllowedMethod(payloadData?.method)))
isProviderApiPrivateOriginDenied({
method: payloadData?.method,
origin,
})
) {
const error = new Error(
`[${origin as string}] is not allowed to call $private methods: ${
Expand Down
20 changes: 19 additions & 1 deletion packages/kit-bg/src/apis/backgroundApiPermissions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { EAtomNames } from '../states/jotai/atomNames';

import { isBackgroundApiAtomWritable } from './backgroundApiPermissions';
import {
isBackgroundApiAtomWritable,
isProviderApiPrivateOriginDenied,
} from './backgroundApiPermissions';

describe('backgroundApiPermissions', () => {
it('blocks UI writes to the background-owned Unifold recipient', () => {
Expand All @@ -23,3 +26,18 @@ describe('backgroundApiPermissions', () => {
);
});
});

describe('isProviderApiPrivateOriginDenied', () => {
it.each([
['wallet_openPrimeSubscription', 'https://app.onekey.so', false],
['wallet_openPrimeSubscription', 'https://app.onekeytest.com', false],
['wallet_openPrimeSubscription', 'https://1key.so', false],
['wallet_openPrimeSubscription', 'https://evil.example', true],
['wallet_openPrimeSubscription', 'https://docs.onekey.so', true],
['wallet_keylessGetStatus', 'https://app.onekey.so', false],
['wallet_keylessGetStatus', 'https://evil.example', true],
['wallet_getConnectWalletInfo', 'https://evil.example', false],
])('method=%s origin=%s denied=%s', (method, origin, denied) => {
expect(isProviderApiPrivateOriginDenied({ method, origin })).toBe(denied);
});
});
32 changes: 32 additions & 0 deletions packages/kit-bg/src/apis/backgroundApiPermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ export const PROVIDER_API_PRIVATE_KEYLESS_METHOD = [
'wallet_keylessDisconnectSite',
];

// First-party web landings only (app.onekey.so / app.onekeytest.com / 1key.so).
// Do not add these to PROVIDER_API_PRIVATE_WHITE_LIST_METHOD — those methods
// are callable from any origin.
export const PROVIDER_API_PRIVATE_FIRST_PARTY_WEB_METHOD = [
'wallet_openPrimeSubscription',
];

// white list method which can be called from any origin
// so these method should NOT return sensitive data
export function isProviderApiPrivateAllowedMethod(method?: string) {
Expand All @@ -94,6 +101,12 @@ export function isProviderApiPrivateKeylessMethod(method?: string) {
return method && PROVIDER_API_PRIVATE_KEYLESS_METHOD.includes(method || '');
}

export function isProviderApiPrivateFirstPartyWebMethod(method?: string) {
return (
!!method && PROVIDER_API_PRIVATE_FIRST_PARTY_WEB_METHOD.includes(method)
);
}

// Dev servers run on arbitrary ports (e.g. http://localhost:3000), but the
// strict allow-lists only contain "http://localhost" without a port. Loosen
// to a startsWith check in dev so the keyless RPCs are reachable locally.
Expand Down Expand Up @@ -122,6 +135,25 @@ export function isProviderApiPrivateAllowedOrigin(origin?: string) {
);
}

export function isProviderApiPrivateOriginDenied({
method,
origin,
}: {
method?: string;
origin?: string;
}) {
if (
isProviderApiPrivateKeylessMethod(method) ||
isProviderApiPrivateFirstPartyWebMethod(method)
) {
return !isProviderApiPrivateAllowedKeylessOrigin(origin);
}
return (
!isProviderApiPrivateAllowedOrigin(origin) &&
!isProviderApiPrivateAllowedMethod(method)
);
}

export function isWebEmbedApiAllowedOrigin(origin?: string) {
return !!origin && WEB_EMBED_API_WHITE_LIST_ORIGIN.includes(origin);
}
Expand Down
31 changes: 30 additions & 1 deletion packages/kit-bg/src/providers/ProviderApiPrivate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
getOneKeyWebUrl,
} from '@onekeyhq/shared/src/config/appConfig';
import type { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConsts';
import {
PRIME_SUBSCRIPTION_EXT_HANDOFF_QUERY,
PRIME_SUBSCRIPTION_EXT_HANDOFF_VALUE,
} from '@onekeyhq/shared/src/consts/deeplinkConsts';
import { OneKeyLocalError } from '@onekeyhq/shared/src/errors';
import type { IEventBusPayloadShowToast } from '@onekeyhq/shared/src/eventBus/appEventBus';
import {
Expand Down Expand Up @@ -47,7 +51,10 @@ import type {
IRookieShareData,
} from '@onekeyhq/shared/types/rookieGuide';

import { isWebEmbedApiAllowedOrigin } from '../apis/backgroundApiPermissions';
import {
isProviderApiPrivateAllowedKeylessOrigin,
isWebEmbedApiAllowedOrigin,
} from '../apis/backgroundApiPermissions';
import { devSettingsPersistAtom } from '../states/jotai/atoms/devSettings';

import ProviderApiBase from './ProviderApiBase';
Expand Down Expand Up @@ -703,6 +710,28 @@ class ProviderApiPrivate extends ProviderApiBase {
return Promise.resolve(this.lastFocusUrl);
}

@providerApiMethod()
async wallet_openPrimeSubscription(request: IJsBridgeMessagePayload) {
if (!platformEnv.isExtension) {
throw new OneKeyLocalError(
'wallet_openPrimeSubscription is only available in the extension',
);
}
if (!isProviderApiPrivateAllowedKeylessOrigin(request.origin)) {
throw new OneKeyLocalError(
`[${request.origin ?? ''}] is not allowed to call wallet_openPrimeSubscription`,
);
}
await this.backgroundApi.serviceApp.openExtensionExpandTab({
path: '/',
params: {
[PRIME_SUBSCRIPTION_EXT_HANDOFF_QUERY]:
PRIME_SUBSCRIPTION_EXT_HANDOFF_VALUE,
},
});
return { success: true };
}

@providerApiMethod()
async wallet_keylessGetStatus(
request: IJsBridgeMessagePayload,
Expand Down
14 changes: 14 additions & 0 deletions packages/kit/src/provider/Bootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ const LazyExtensionMarketTokenDetailHashNavigation =
})
: null;

const LazyExtPrimeSubscriptionHandoffOnMount =
platformEnv.isExtensionUiExpandTab
? lazy(async () => {
const { ExtPrimeSubscriptionHandoffOnMount } =
await import('../routes/config/deeplink/ExtPrimeSubscriptionHandoffOnMount');
return { default: ExtPrimeSubscriptionHandoffOnMount };
})
: null;

// useAppUpdateInfo no longer accepts `autoCheck` — first-launch dispatch
// and AppState 'active' resume listener now live in <AppUpdateForeground />,
// mounted once below in Bootstrap's render output. Existing callers in
Expand Down Expand Up @@ -1054,6 +1063,11 @@ export function Bootstrap() {
<LazyExtensionMarketTokenDetailHashNavigation />
</Suspense>
) : null}
{LazyExtPrimeSubscriptionHandoffOnMount ? (
<Suspense fallback={null}>
<LazyExtPrimeSubscriptionHandoffOnMount />
</Suspense>
) : null}
{platformEnv.isDesktopMac ? <DesktopTrayDataProvider /> : null}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { memo, useEffect } from 'react';

import { EOneKeyDeepLinkPath } from '@onekeyhq/shared/src/consts/deeplinkConsts';
import platformEnv from '@onekeyhq/shared/src/platformEnv';
import timerUtils from '@onekeyhq/shared/src/utils/timerUtils';
import uriUtils from '@onekeyhq/shared/src/utils/uriUtils';

import { consumePrimeSubscriptionHandoffFromUrl } from './primeSubscriptionExtHandoff';

function ExtPrimeSubscriptionHandoffOnMountCmp() {
useEffect(() => {
if (!platformEnv.isExtensionUiExpandTab) {
return;
}

const runPendingHandoff = () => {
if (!consumePrimeSubscriptionHandoffFromUrl()) {
return;
}
void (async () => {
await timerUtils.wait(600);
const { handleDeepLinkUrl } = await import('.');
handleDeepLinkUrl({
url: uriUtils.buildDeepLinkUrl({
path: EOneKeyDeepLinkPath.prime_subscription,
}),
});
})();
};

runPendingHandoff();
globalThis.addEventListener('hashchange', runPendingHandoff);
return () => {
globalThis.removeEventListener('hashchange', runPendingHandoff);
};
}, []);

return null;
}

export const ExtPrimeSubscriptionHandoffOnMount = memo(
ExtPrimeSubscriptionHandoffOnMountCmp,
);
77 changes: 77 additions & 0 deletions packages/kit/src/routes/config/deeplink/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
/* cspell:ignore Infini */
import { perpsCommonConfigPersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms';
import appGlobals from '@onekeyhq/shared/src/appGlobals';
import { EOneKeyDeepLinkPath } from '@onekeyhq/shared/src/consts/deeplinkConsts';
import {
EModalRoutes,
ERootRoutes,
ETabMarketRoutes,
ETabRoutes,
ETabSwapRoutes,
} from '@onekeyhq/shared/src/routes';
import { EPrimePages } from '@onekeyhq/shared/src/routes/prime';

import { handleDeepLinkUrl } from '..';
import {
Expand Down Expand Up @@ -35,12 +39,22 @@ jest.mock('../../../../background/instance/backgroundApiProxy', () => ({
serviceDevSetting: {
getDevSetting: jest.fn(async () => ({ settings: {} })),
},
servicePrime: {
isLoggedIn: jest.fn(async () => false),
},
serviceApp: {
isAppLocked: jest.fn(async () => false),
},
walletConnect: {
connectToDapp: jest.fn(),
},
},
}));

jest.mock('../../../../utils/passwordUtils', () => ({
whenAppUnlocked: jest.fn(async () => undefined),
}));

jest.mock('../../../../views/Home/pages/urlAccount/urlAccountUtils', () => ({
urlAccountNavigation: {
pushUrlAccountPageFromDeeplink: jest.fn(),
Expand Down Expand Up @@ -343,3 +357,66 @@ describe('stocks / perps universal links', () => {
expect(switchTabAsync).not.toHaveBeenCalled();
});
});

describe('prime_subscription deep link', () => {
const pushModal = jest.fn();
const originalRootAppNavigation = appGlobals.$rootAppNavigation;
const backgroundApiProxy = jest.requireMock(
'../../../../background/instance/backgroundApiProxy',
).default as {
servicePrime: { isLoggedIn: jest.Mock };
};

beforeEach(() => {
jest.clearAllMocks();
appGlobals.$rootAppNavigation = {
pushModal,
} as unknown as typeof appGlobals.$rootAppNavigation;
backgroundApiProxy.servicePrime.isLoggedIn.mockResolvedValue(false);
});

afterEach(() => {
appGlobals.$rootAppNavigation = originalRootAppNavigation;
});

it('opens Prime dashboard when the OneKey ID session is missing', async () => {
handleDeepLinkUrl({
url: `onekey-wallet://${EOneKeyDeepLinkPath.prime_subscription}`,
});
await flushAsyncTasks();
await flushAsyncTasks();

expect(pushModal).toHaveBeenCalledWith(EModalRoutes.PrimeModal, {
screen: EPrimePages.PrimeDashboard,
params: { fromDeepLink: true },
});
});

it.each([
'https://app.onekey.so/prime/subscription',
'https://app.onekeytest.com/prime/subscription',
])(
'does not treat the web landing URL as an app deep link: %s',
async (url) => {
handleDeepLinkUrl({ url });
await flushAsyncTasks();
await flushAsyncTasks();

expect(pushModal).not.toHaveBeenCalled();
},
);

it('opens the Infini subscription page when already logged in', async () => {
backgroundApiProxy.servicePrime.isLoggedIn.mockResolvedValue(true);

handleDeepLinkUrl({
url: `onekey-wallet://${EOneKeyDeepLinkPath.prime_subscription}`,
});
await flushAsyncTasks();
await flushAsyncTasks();

expect(pushModal).toHaveBeenCalledWith(EModalRoutes.PrimeModal, {
screen: EPrimePages.PrimeInfiniSubscription,
});
});
});
Loading
Loading