const TabNavigator = createLeftTabNavigator()
const makeOptions = (rd: RouteDef) => {
return ({route, navigation}: GetOptionsParams) => {
@@ -104,11 +100,51 @@ const makeOptions = (rd: RouteDef) => {
return {...opt}
}
}
-const cryptoScreens = routeMapToScreenElements(cryptoSubRoutes, TabNavigator.Screen, makeLayout, makeOptions, false, false, false)
-const CryptoSubNavigator = () => (
+const cryptoScreens = routeMapToScreenElements(
+ cryptoSubRoutes,
+ TabNavigator.Screen,
+ makeLayout,
+ makeOptions,
+ false,
+ false,
+ false
+)
+const DesktopCryptoSubNavigator = () => (
{cryptoScreens}
)
-export default CryptoSubNavigator
+const NativeCryptoSubNav = () => {
+ const {navigate} = C.useNav()
+ return (
+
+ {Crypto.Tabs.map(t => (
+ navigate(t.tab)}
+ />
+ ))}
+
+ )
+}
+
+const styles = Kb.Styles.styleSheetCreate(
+ () =>
+ ({
+ box: {backgroundColor: Kb.Styles.globalColors.white},
+ container: {
+ backgroundColor: Kb.Styles.globalColors.blueGrey,
+ paddingLeft: Kb.Styles.globalMargins.small,
+ paddingRight: Kb.Styles.globalMargins.small,
+ paddingTop: Kb.Styles.globalMargins.xsmall,
+ },
+ nav: {width: 180},
+ }) as const
+)
+
+export default isMobile ? NativeCryptoSubNav : DesktopCryptoSubNavigator
diff --git a/shared/desktop/app/app-events.desktop.tsx b/shared/desktop/app/app-events.desktop.tsx
index d00b36f5f287..797f9612e748 100644
--- a/shared/desktop/app/app-events.desktop.tsx
+++ b/shared/desktop/app/app-events.desktop.tsx
@@ -3,10 +3,10 @@ import * as R from '@/constants/remote'
import * as RemoteGen from '@/constants/remote-actions'
import logger from '@/logger'
import os from 'os'
-import {isLinux, isWindows, cacheRoot} from '@/constants/platform.desktop'
+import {isLinux, isWindows, cacheRoot} from '@/constants/platform'
import {ctlQuit} from './ctl.desktop'
import {allowMultipleInstances} from '@/local-debug'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
const {env} = KB2.constants
const isPathSaltpack = (p: string) =>
diff --git a/shared/desktop/app/dynamic-config.tsx b/shared/desktop/app/dynamic-config.tsx
index a244bc638305..63ac9742b795 100644
--- a/shared/desktop/app/dynamic-config.tsx
+++ b/shared/desktop/app/dynamic-config.tsx
@@ -1,5 +1,5 @@
import fs from 'fs'
-import {serverConfigFileName, jsonDebugFileName} from '@/constants/platform.desktop'
+import {serverConfigFileName, jsonDebugFileName} from '@/constants/platform'
const getConfigOverload = () => {
let config: {[key: string]: unknown} = {}
// Load overrides from server config
diff --git a/shared/desktop/app/exec.desktop.tsx b/shared/desktop/app/exec.desktop.tsx
index ccea772fbbc6..29be79affe18 100644
--- a/shared/desktop/app/exec.desktop.tsx
+++ b/shared/desktop/app/exec.desktop.tsx
@@ -2,7 +2,7 @@ import {exec as _exec, type ExecException} from 'child_process'
import fs from 'fs'
import os from 'os'
-import {runMode} from '@/constants/platform.desktop'
+import {runMode} from '@/constants/platform'
// Execute at path with args.
// If you specify platformOnly or runModeOnly, then callback will be called
diff --git a/shared/desktop/app/ipc-handlers.desktop.tsx b/shared/desktop/app/ipc-handlers.desktop.tsx
index 713527091485..c8099382e204 100644
--- a/shared/desktop/app/ipc-handlers.desktop.tsx
+++ b/shared/desktop/app/ipc-handlers.desktop.tsx
@@ -1,4 +1,4 @@
-import KB2, {type OpenDialogOptions, type SaveDialogOptions} from '@/util/electron.desktop'
+import KB2, {type OpenDialogOptions, type SaveDialogOptions} from '@/util/electron'
import {showDockIcon, closeWindows, getMainWindow} from './main-window.desktop'
import * as Electron from 'electron'
import * as R from '@/constants/remote'
@@ -9,7 +9,7 @@ import path from 'path'
import fse from 'fs-extra'
import {spawn, execFile, exec} from 'child_process'
import startWinService from './start-win-service.desktop'
-import {isDarwin, isLinux, isWindows, socketPath, fileUIName, dokanPath, windowsBinPath} from '@/constants/platform.desktop'
+import {isDarwin, isLinux, isWindows, socketPath, fileUIName, dokanPath, windowsBinPath} from '@/constants/platform'
import {ctlQuit} from './ctl.desktop'
import logger from '@/logger'
import {htmlURL, preloadPath} from './html-root.desktop'
diff --git a/shared/desktop/app/ipctypes.tsx b/shared/desktop/app/ipctypes.tsx
index a15755efedfa..233fa8d77a19 100644
--- a/shared/desktop/app/ipctypes.tsx
+++ b/shared/desktop/app/ipctypes.tsx
@@ -1,4 +1,4 @@
-import type {OpenDialogOptions, SaveDialogOptions} from '@/util/electron.desktop'
+import type {OpenDialogOptions, SaveDialogOptions} from '@/util/electron'
import type * as RPCTypes from '@/constants/rpc/rpc-gen'
export type Action =
diff --git a/shared/desktop/app/kb2-impl.desktop.tsx b/shared/desktop/app/kb2-impl.desktop.tsx
index 6f902f671b77..f97779002352 100644
--- a/shared/desktop/app/kb2-impl.desktop.tsx
+++ b/shared/desktop/app/kb2-impl.desktop.tsx
@@ -2,7 +2,7 @@
import {app, nativeTheme} from 'electron'
import os from 'os'
import path from 'path'
-import type {KB2} from '@/util/electron.desktop'
+import type {KB2} from '@/util/electron'
const {env, argv, pid} = process
const platform = process.platform
diff --git a/shared/desktop/app/main-window.desktop.tsx b/shared/desktop/app/main-window.desktop.tsx
index e3592f16e1fa..97555e245ccd 100644
--- a/shared/desktop/app/main-window.desktop.tsx
+++ b/shared/desktop/app/main-window.desktop.tsx
@@ -4,11 +4,11 @@ import * as R from '@/constants/remote'
import * as fs from 'fs'
import menuHelper from './menu-helper.desktop'
import {showDevTools} from '@/local-debug'
-import {guiConfigFilename, isDarwin, isWindows, defaultUseNativeFrame} from '@/constants/platform.desktop'
+import {guiConfigFilename, isDarwin, isWindows, defaultUseNativeFrame} from '@/constants/platform'
import logger from '@/logger'
import debounce from 'lodash/debounce'
import {htmlURL, preloadPath} from './html-root.desktop'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
const {env} = KB2.constants
diff --git a/shared/desktop/app/menu-bar.desktop.tsx b/shared/desktop/app/menu-bar.desktop.tsx
index 437a39a36e81..008a87057df1 100644
--- a/shared/desktop/app/menu-bar.desktop.tsx
+++ b/shared/desktop/app/menu-bar.desktop.tsx
@@ -3,7 +3,7 @@ import * as RemoteGen from '@/constants/remote-actions'
import * as R from '@/constants/remote'
import * as Electron from 'electron'
import logger from '@/logger'
-import {isDarwin, isWindows, isLinux} from '@/constants/platform.desktop'
+import {isDarwin, isWindows, isLinux} from '@/constants/platform'
import {menubar} from 'menubar'
import {showDevTools, skipSecondaryDevtools} from '@/local-debug'
import {getMainWindow} from './main-window.desktop'
diff --git a/shared/desktop/app/node.desktop.tsx b/shared/desktop/app/node.desktop.tsx
index 31952b1cde3e..f5d8b55adfe8 100644
--- a/shared/desktop/app/node.desktop.tsx
+++ b/shared/desktop/app/node.desktop.tsx
@@ -3,8 +3,8 @@ import '../renderer/preload.desktop'
import * as Electron from 'electron'
import * as R from '@/constants/remote'
import * as RemoteGen from '@/constants/remote-actions'
-import {isDarwin} from '@/constants/platform.desktop'
-import KB2 from '@/util/electron.desktop'
+import {isDarwin} from '@/constants/platform'
+import KB2 from '@/util/electron'
import {configOverload} from './dynamic-config'
import MainWindow from './main-window.desktop'
import devTools from './dev-tools.desktop'
diff --git a/shared/desktop/app/paths.desktop.tsx b/shared/desktop/app/paths.desktop.tsx
index 9043d6d2ea38..24ef65b8aa89 100644
--- a/shared/desktop/app/paths.desktop.tsx
+++ b/shared/desktop/app/paths.desktop.tsx
@@ -1,6 +1,6 @@
import os from 'os'
import path from 'path'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
import {app} from 'electron'
const {env} = KB2.constants
diff --git a/shared/desktop/remote/component-loader.desktop.tsx b/shared/desktop/remote/component-loader.desktop.tsx
index d9aa41e782b4..262c613893dd 100644
--- a/shared/desktop/remote/component-loader.desktop.tsx
+++ b/shared/desktop/remote/component-loader.desktop.tsx
@@ -6,8 +6,8 @@ import * as Kb from '@/common-adapters'
import {GlobalKeyEventHandler} from '@/common-adapters/key-event-handler.desktop'
import {disableDragDrop} from '@/util/drag-drop.desktop'
import ErrorBoundary from '@/common-adapters/error-boundary'
-import {initDesktopStyles} from '@/styles/index.desktop'
-import KB2 from '@/util/electron.desktop'
+import {initDesktopStyles} from '@/styles'
+import KB2 from '@/util/electron'
import {setServiceDecoration} from '@/common-adapters/markdown/react'
import ServiceDecoration from '@/common-adapters/markdown/service-decoration'
import {type RemoteComponentName, useRemotePropsReceiver} from './remote-component.desktop'
diff --git a/shared/desktop/remote/remote-component.desktop.tsx b/shared/desktop/remote/remote-component.desktop.tsx
index b1395a554c5e..5104fac159af 100644
--- a/shared/desktop/remote/remote-component.desktop.tsx
+++ b/shared/desktop/remote/remote-component.desktop.tsx
@@ -3,7 +3,7 @@ import logger from '@/logger'
import * as R from '@/constants/remote'
import * as RemoteGen from '@/constants/remote-actions'
import {useDarkModeState} from '@/stores/darkmode'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
const {ipcRendererOn, showInactive} = KB2.functions
@@ -22,7 +22,7 @@ type RemotePropsReceiverState = {
value: P | null
}
-export const getRemoteComponentParam = () => new URLSearchParams(window.location.search).get('param') ?? ''
+export const getRemoteComponentParam = () => new URLSearchParams(window!.location.search).get('param') ?? ''
export const useRemoteDarkModeSync = (darkMode: boolean) => {
const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode)
diff --git a/shared/desktop/remote/use-browser-window.desktop.tsx b/shared/desktop/remote/use-browser-window.desktop.tsx
index 5062af3338ec..c3724814e7ee 100644
--- a/shared/desktop/remote/use-browser-window.desktop.tsx
+++ b/shared/desktop/remote/use-browser-window.desktop.tsx
@@ -1,6 +1,6 @@
// This hook creates a remote brower window when mounted
import * as React from 'react'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
import type {RemoteComponentName} from './remote-component.desktop'
const {makeRenderer, closeRenderer} = KB2.functions
diff --git a/shared/desktop/remote/use-serialize-props.desktop.tsx b/shared/desktop/remote/use-serialize-props.desktop.tsx
index 7f4b9a393ed5..3670bad2f9a5 100644
--- a/shared/desktop/remote/use-serialize-props.desktop.tsx
+++ b/shared/desktop/remote/use-serialize-props.desktop.tsx
@@ -2,7 +2,7 @@
// Listens for requests from the main process (which proxies requests from other windows) to kick off an update
import * as React from 'react'
import * as C from '@/constants'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
import {useConfigState} from '@/stores/config'
import type {RemoteComponentName} from './remote-component.desktop'
diff --git a/shared/desktop/renderer/main.desktop.tsx b/shared/desktop/renderer/main.desktop.tsx
index f216bfdfa816..14041f85fe06 100644
--- a/shared/desktop/renderer/main.desktop.tsx
+++ b/shared/desktop/renderer/main.desktop.tsx
@@ -2,7 +2,7 @@
import './globals.desktop'
import {isDarwin, isWindows} from '@/constants/platform'
import '@/util/why-did-you-render'
-import KB2, {waitOnKB2Loaded} from '@/util/electron.desktop'
+import KB2, {waitOnKB2Loaded} from '@/util/electron'
import * as DarkMode from '@/stores/darkmode'
waitOnKB2Loaded(() => {
diff --git a/shared/desktop/renderer/main2.desktop.tsx b/shared/desktop/renderer/main2.desktop.tsx
index c32fab788d33..bd15e2855c96 100644
--- a/shared/desktop/renderer/main2.desktop.tsx
+++ b/shared/desktop/renderer/main2.desktop.tsx
@@ -1,6 +1,6 @@
///
// Entry point to the chrome part of the app
-import Main from '@/app/main.desktop'
+import Main from '@/app/main'
// order of the above must NOT change. needed for patching / hot loading to be correct
import * as C from '@/constants'
import * as React from 'react'
@@ -9,17 +9,17 @@ import type * as RemoteGen from '@/constants/remote-actions'
import {GlobalKeyEventHandler} from '@/common-adapters/key-event-handler.desktop'
import {makeEngine} from '@/engine'
import {disableDragDrop} from '@/util/drag-drop.desktop'
-import {initDesktopStyles} from '@/styles/index.desktop'
+import {initDesktopStyles} from '@/styles'
import {isWindows} from '@/constants/platform'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
import {useConfigState} from '@/stores/config'
import {useShellState} from '@/stores/shell'
import {setServiceDecoration} from '@/common-adapters/markdown/react'
import ServiceDecoration from '@/common-adapters/markdown/service-decoration'
import {useDarkModeState} from '@/stores/darkmode'
-import {initPlatformListener, onEngineIncoming} from '@/constants/init/index.desktop'
+import {initPlatformListener, onEngineIncoming} from '@/constants/init/index'
import {eventFromRemoteWindows} from './remote-event-handler.desktop'
-import type {default as NewMainType} from '../../app/main.desktop'
+import type {default as NewMainType} from '../../app/main'
import {dumpLogs} from '@/util/storeless-actions'
setServiceDecoration(ServiceDecoration)
@@ -167,12 +167,12 @@ const setupHMR = () => {
const refreshMain = () => {
try {
- const {default: NewMain} = require('../../app/main.desktop') as {default: typeof NewMainType}
+ const {default: NewMain} = require('../../app/main') as {default: typeof NewMainType}
render(NewMain)
} catch {}
}
- module.hot.accept(['../../app/main.desktop'], refreshMain)
+ module.hot.accept(['../../app/main'], refreshMain)
module.hot.accept(['../../common-adapters/index'], () => {})
}
diff --git a/shared/desktop/renderer/preload.desktop.tsx b/shared/desktop/renderer/preload.desktop.tsx
index ed35dca55ce4..e76e48730c34 100644
--- a/shared/desktop/renderer/preload.desktop.tsx
+++ b/shared/desktop/renderer/preload.desktop.tsx
@@ -6,7 +6,7 @@ import {
type KB2,
type OpenDialogOptions,
type SaveDialogOptions,
-} from '@/util/electron.desktop'
+} from '@/util/electron'
import type * as RPCTypes from '@/constants/rpc/rpc-gen'
import type {Action} from '../app/ipctypes'
diff --git a/shared/desktop/renderer/remote-event-handler.desktop.tsx b/shared/desktop/renderer/remote-event-handler.desktop.tsx
index be6c16c4fe82..da90aebb720c 100644
--- a/shared/desktop/renderer/remote-event-handler.desktop.tsx
+++ b/shared/desktop/renderer/remote-event-handler.desktop.tsx
@@ -5,7 +5,7 @@ import * as Tabs from '@/constants/tabs'
import {RPCError} from '@/util/errors'
import {ignorePromise} from '@/constants/utils'
import {navigateAppend, navigateToThread, previewConversation, switchTab} from '@/constants/router'
-import {onEngineConnected, onEngineDisconnected} from '@/constants/init/index.desktop'
+import {onEngineConnected, onEngineDisconnected} from '@/constants/init/index'
import {emitDeepLink} from '@/router-v2/linking'
import {isPathSaltpackEncrypted, isPathSaltpackSigned} from '@/util/path'
import type HiddenString from '@/util/hidden-string'
diff --git a/shared/desktop/webpack.config.mts b/shared/desktop/webpack.config.mts
index fbe8bd5d4043..7b2ea6fcf88e 100644
--- a/shared/desktop/webpack.config.mts
+++ b/shared/desktop/webpack.config.mts
@@ -66,7 +66,13 @@ if (debugWebpack) {
}
const makeAlias = (isDev: boolean): Record => {
- const alias = ignoredModules.reduce>(
+ // Sort longest-first so subpath entries (e.g. 'foo/bar') are inserted into
+ // the alias object before their parent package ('foo'). webpack's enhanced-resolve
+ // checks aliases in insertion order and uses the first match; a shorter prefix
+ // like 'foo' would otherwise intercept 'foo/bar' and append '/bar' to the
+ // null-module path, producing a non-existent path.
+ const sortedModules = [...ignoredModules].sort((a, b) => b.length - a.length)
+ const alias = sortedModules.reduce>(
(acc, name: string) => {
acc[name] = nullModulePath
return acc
@@ -169,7 +175,14 @@ const makeRules = ({
}) satisfies RuleSetRule
),
{
- exclude: /\/dist\//,
+ // Native-only files must never be parsed by webpack on desktop: they use @/ imports
+ // that babel-module-resolver can't transform (babel ignores *.native.* files), so
+ // webpack would see the raw @/ alias and fail to resolve it.
+ test: /\.(native|ios|android)\.(ts|js)x?$/,
+ use: ['null-loader'],
+ },
+ {
+ exclude: [/\/dist\//, /\.(native|ios|android)\.(ts|js)x?$/],
test: /\.(ts|js)x?$/,
use: makeBabelLoader(isDev, isHot, nodeThread),
},
diff --git a/shared/engine/index.platform.native.tsx b/shared/engine/index.platform.native.tsx
deleted file mode 100644
index 8203e3988881..000000000000
--- a/shared/engine/index.platform.native.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import {LocalTransport, sharedCreateClient, rpcLog} from './transport-shared'
-import type {IncomingRPCCallbackType, ConnectDisconnectCB, CreateClientType} from './index.platform.shared'
-import type {RPCMessage} from './rpc-transport'
-import logger from '@/logger'
-import {engineReset, getNativeEmitter, notifyJSReady} from 'react-native-kb'
-
-class NativeTransport extends LocalTransport {
- protected writeMessage(message: RPCMessage) {
- try {
- if (!global.rpcOnGo) {
- logger.error('>>>> rpcOnGo send before rpcOnGo global?')
- }
- global.rpcOnGo?.(message)
- } catch (e) {
- logger.error('>>>> rpcOnGo JS thrown!', e)
- }
- }
-}
-
-function createClient(
- incomingRPCCallback: IncomingRPCCallbackType,
- connectCallback: ConnectDisconnectCB,
- disconnectCallback: ConnectDisconnectCB
-) {
- const client = sharedCreateClient(
- new NativeTransport(incomingRPCCallback, connectCallback, disconnectCallback)
- )
-
- global.rpcOnJs = (objs: unknown, count: number) => {
- try {
- if (count > 1) {
- const arr = objs as Array
- for (const obj of arr) {
- client.transport.dispatchDecodedMessage(obj)
- }
- } else {
- client.transport.dispatchDecodedMessage(objs)
- }
- } catch (e) {
- logger.error('>>>> rpcOnJs JS thrown!', e)
- }
- }
-
- const RNEmitter = getNativeEmitter()
- RNEmitter.addListener('kb-meta-engine-event', (payload: string) => {
- try {
- switch (payload) {
- case 'kb-engine-reset':
- connectCallback()
- }
- } catch (e) {
- logger.error('>>>> meta engine event JS thrown!', e)
- }
- })
-
- // Signal that JS is ready to send/receive RPCs
- // This sets up native infrastructure and starts bidirectional communication
- logger.info('JS engine ready, notifying native side')
- notifyJSReady()
-
- return client
-}
-
-function resetClient(client: CreateClientType, _ic?: IncomingRPCCallbackType, _cc?: ConnectDisconnectCB, _dc?: ConnectDisconnectCB) {
- // Tell the RN bridge to reset itself
- engineReset()
- return client
-}
-
-export type {CreateClientType, PayloadType, InvokeType} from './index.platform.shared'
-export {resetClient, createClient, rpcLog}
diff --git a/shared/engine/index.platform.desktop.tsx b/shared/engine/index.platform.tsx
similarity index 70%
rename from shared/engine/index.platform.desktop.tsx
rename to shared/engine/index.platform.tsx
index 65c55d50a0de..59f90da7999b 100644
--- a/shared/engine/index.platform.desktop.tsx
+++ b/shared/engine/index.platform.tsx
@@ -1,16 +1,15 @@
-import type {Socket} from 'net'
import logger from '@/logger'
import {TransportShared, LocalTransport, sharedCreateClient, rpcLog} from './transport-shared'
-import {socketPath} from '@/constants/platform.desktop'
-import {printRPCBytes} from '@/local-debug'
import type {CreateClientType, IncomingRPCCallbackType, ConnectDisconnectCB} from './index.platform.shared'
import type {RPCMessage} from './rpc-transport'
-import KB2 from '@/util/electron.desktop'
-
-const {engineSend, ipcRendererOn, mainWindowDispatchEngineIncoming} = KB2.functions
-const {isRenderer} = KB2.constants
+import KB2 from '@/util/electron'
+import type {Socket} from 'net'
+import {printRPCBytes} from '@/local-debug'
+import {socketPath} from '@/constants/platform'
+import {getNativeEmitter, notifyJSReady, engineReset} from 'react-native-kb'
// used by node
+// Desktop transport — only instantiated when !isMobile
class NativeTransport extends TransportShared {
private _socket?: Socket
private _reconnectTimer?: ReturnType
@@ -50,6 +49,7 @@ class NativeTransport extends TransportShared {
}
override packetizeData(m: Uint8Array) {
+ const {mainWindowDispatchEngineIncoming} = KB2.functions
if (printRPCBytes) {
logger.debug('[RPC] Read', m.length)
}
@@ -135,15 +135,73 @@ class NativeTransport extends TransportShared {
class ProxyNativeTransport extends LocalTransport {
protected writeMessage(message: RPCMessage) {
+ const {engineSend} = KB2.functions
engineSend?.(message)
}
}
+// Mobile transport — only instantiated when isMobile
+class NativeTransportMobile extends LocalTransport {
+ protected writeMessage(message: RPCMessage) {
+ try {
+ if (!global.rpcOnGo) {
+ logger.error('>>>> rpcOnGo send before rpcOnGo global?')
+ }
+ global.rpcOnGo?.(message)
+ } catch (e) {
+ logger.error('>>>> rpcOnGo JS thrown!', e)
+ }
+ }
+}
+
function createClient(
incomingRPCCallback: IncomingRPCCallbackType,
connectCallback: ConnectDisconnectCB,
disconnectCallback: ConnectDisconnectCB
) {
+ if (isMobile) {
+ const client = sharedCreateClient(
+ new NativeTransportMobile(incomingRPCCallback, connectCallback, disconnectCallback)
+ )
+
+ global.rpcOnJs = (objs: unknown, count: number) => {
+ try {
+ if (count > 1) {
+ const arr = objs as Array
+ for (const obj of arr) {
+ client.transport.dispatchDecodedMessage(obj)
+ }
+ } else {
+ client.transport.dispatchDecodedMessage(objs)
+ }
+ } catch (e) {
+ logger.error('>>>> rpcOnJs JS thrown!', e)
+ }
+ }
+
+ const RNEmitter = getNativeEmitter()
+ RNEmitter.addListener('kb-meta-engine-event', (payload: string) => {
+ try {
+ switch (payload) {
+ case 'kb-engine-reset':
+ connectCallback()
+ }
+ } catch (e) {
+ logger.error('>>>> meta engine event JS thrown!', e)
+ }
+ })
+
+ // Signal that JS is ready to send/receive RPCs
+ // This sets up native infrastructure and starts bidirectional communication
+ logger.info('JS engine ready, notifying native side')
+ notifyJSReady()
+
+ return client
+ }
+
+ const {ipcRendererOn} = KB2.functions
+ const {isRenderer} = KB2.constants
+
if (!isRenderer) {
return sharedCreateClient(new NativeTransport(incomingRPCCallback, connectCallback, disconnectCallback))
} else {
@@ -170,6 +228,14 @@ function resetClient(
connectCallback: ConnectDisconnectCB,
disconnectCallback: ConnectDisconnectCB
) {
+ if (isMobile) {
+ // Tell the RN bridge to reset itself
+ engineReset()
+ return client
+ }
+
+ const {isRenderer} = KB2.constants
+
if (isRenderer) {
client.transport.reset()
return client
diff --git a/shared/globals.native.d.ts b/shared/globals.native.d.ts
index 21ee84887305..bb7c3f2c3983 100644
--- a/shared/globals.native.d.ts
+++ b/shared/globals.native.d.ts
@@ -9,9 +9,21 @@ declare var window:
opts?: {timeout?: number}
) => number
cancelIdleCallback?: (handle: number) => void
+ addEventListener: (event: string, cb: () => void) => void
+ location: {search: string}
}
| undefined
+// Stub for Web Notification API used in merged util/misc.tsx (desktop-only at runtime)
+declare class Notification {
+ onclick: (() => void) | null
+ onclose: (() => void) | null
+ constructor(title: string, options?: {body?: string; silent?: boolean})
+}
+
+// Stub for navigator.onLine used in merged desktop-only init code (guarded by !isMobile)
+declare var navigator: {onLine: boolean}
+
// Minimal File/DataTransfer stubs for shared files that use these types
// (actual drag-and-drop only runs on desktop, but the code is in shared files)
interface File {
@@ -22,3 +34,98 @@ interface DataTransfer {
readonly files: ReadonlyArray
readonly types: ReadonlyArray
}
+
+// Stubs for DOM observer/element types used in merged platform files.
+// Desktop build excludes this file so declare class is safe (no conflict with lib.dom).
+interface Element {}
+interface HTMLElement extends Element {}
+interface HTMLDivElement extends HTMLElement {
+ clientWidth?: number
+}
+
+interface DOMRectReadOnly {
+ readonly width: number
+ readonly height: number
+ readonly top: number
+ readonly left: number
+ readonly bottom: number
+ readonly right: number
+ readonly x: number
+ readonly y: number
+}
+
+interface ResizeObserverEntry {
+ readonly target: Element
+ readonly contentRect: DOMRectReadOnly
+}
+declare class ResizeObserver {
+ constructor(callback: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void)
+ disconnect(): void
+ observe(target: Element): void
+ unobserve(target: Element): void
+}
+
+type IntersectionObserverCallback = (
+ entries: IntersectionObserverEntry[],
+ observer: IntersectionObserver
+) => void
+interface IntersectionObserverInit {
+ root?: Element | Document | null
+ rootMargin?: string
+ threshold?: number | number[]
+}
+interface IntersectionObserverEntry {
+ readonly isIntersecting: boolean
+ readonly target: Element
+ readonly time: number
+ readonly intersectionRatio: number
+ readonly rootBounds: DOMRectReadOnly | null
+ readonly boundingClientRect: DOMRectReadOnly
+ readonly intersectionRect: DOMRectReadOnly
+}
+declare class IntersectionObserver {
+ readonly POLL_INTERVAL: number | null | undefined
+ readonly USE_MUTATION_OBSERVER: boolean | undefined
+ constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit)
+ observe(target: Element): void
+ unobserve(target: Element): void
+ disconnect(): void
+}
+
+declare function requestAnimationFrame(callback: FrameRequestCallback): number
+type FrameRequestCallback = (time: number) => void
+
+// Stub for keyboard event key property used in desktop key handler (desktop-only at runtime)
+interface KeyboardEvent {
+ readonly key: string
+}
+
+// Stub for document used in merged initDesktopStyles (desktop-only at runtime, guarded by !isMobile)
+interface DOMNode {
+ firstChild: DOMElement | null
+ appendChild(child: DOMNode): DOMNode
+ removeChild(child: DOMNode): DOMNode
+}
+interface DOMElement extends DOMNode {
+ setAttribute(k: string, v: string): void
+ classList: {add(c: string): void}
+ clientWidth: number
+ appendChild(child: DOMNode): DOMNode
+}
+interface DOMTextNode extends DOMNode {}
+declare var document: {
+ activeElement: unknown
+ addEventListener(type: string, listener: (e: any) => void, useCapture?: boolean): void
+ removeEventListener(type: string, listener: (e: any) => void, useCapture?: boolean): void
+ getElementById(id: string): DOMElement | null
+ head: {appendChild(el: DOMElement): void}
+ body: {
+ appendChild(el: DOMElement): void
+ removeChild(el: DOMElement): void
+ classList: {add(c: string): void}
+ addEventListener(type: string, listener: (e: any) => void, useCapture?: boolean): void
+ removeEventListener(type: string, listener: (e: any) => void, useCapture?: boolean): void
+ }
+ createElement(tag: string): DOMElement
+ createTextNode(text: string): DOMTextNode
+}
diff --git a/shared/local-debug.tsx b/shared/local-debug.tsx
index f4e389a63ed8..eeb8ff7030c5 100644
--- a/shared/local-debug.tsx
+++ b/shared/local-debug.tsx
@@ -48,7 +48,7 @@ if (__DEV__) {
}
if (!isMobile) {
- const KB2 = require('./util/electron.desktop').default as {
+ const KB2 = require('./util/electron').default as {
constants: {configOverload?: Partial}
}
config = {...config, ...KB2.constants.configOverload}
diff --git a/shared/login/relogin/index.desktop.tsx b/shared/login/relogin/index.desktop.tsx
deleted file mode 100644
index c9c30bf84373..000000000000
--- a/shared/login/relogin/index.desktop.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import * as C from '@/constants'
-import * as React from 'react'
-import * as Kb from '@/common-adapters'
-import UserCard from '../user-card'
-import {errorBanner, SignupScreen} from '@/signup/common'
-import type {Props} from './index.shared'
-
-const other = 'Someone else...'
-
-const UserRow = ({user, hasStoredSecret}: {user: string; hasStoredSecret: boolean}) => (
-
-
- {user}
-
- {hasStoredSecret && • Signed in}
-
-)
-
-const Login = (props: Props) => {
- const _inputRef = React.useRef(null)
-
- const _onClickUserIdx = (selected: number) => {
- const user = props.users.at(selected)
- if (!user) {
- props.onSomeoneElse()
- } else {
- props.selectedUserChange(user.username)
- if (_inputRef.current) {
- _inputRef.current.focus()
- }
- }
- }
-
- const userRows = props.users
- .concat({hasStoredSecret: false, uid: '', username: other})
- .map(u => )
-
- const selectedIdx = props.users.findIndex(u => u.username === props.selectedUser)
- return (
-
-
-
-
- {props.needPassword && (
-
-
-
- )}
-
-
- Forgot password?
-
-
-
-
-
-
-
-
- )
-}
-
-const styles = Kb.Styles.styleSheetCreate(
- () =>
- ({
- container: {
- ...Kb.Styles.globalStyles.flexBoxColumn,
- alignItems: 'center',
- flex: 1,
- justifyContent: 'center',
- },
- contentBox: {
- alignSelf: 'center',
- maxWidth: 460,
- padding: Kb.Styles.globalMargins.small,
- },
- forgotPassword: {
- marginTop: Kb.Styles.globalMargins.tiny,
- },
- header: {
- borderBottomWidth: 0,
- },
- inputRow: {
- marginBottom: 0,
- marginTop: Kb.Styles.globalMargins.tiny,
- width: '100%',
- },
- loginSubmitButton: {
- marginTop: 0,
- maxHeight: 32,
- width: '100%',
- },
- other: {color: Kb.Styles.globalColors.black},
- provisioned: {color: Kb.Styles.globalColors.orange},
- userContainer: {
- backgroundColor: Kb.Styles.globalColors.transparent,
- flex: 1,
- },
- userDropdown: {
- backgroundColor: Kb.Styles.globalColors.white,
- width: '100%',
- },
- userOverlayStyle: {
- backgroundColor: Kb.Styles.globalColors.white,
- width: 348,
- },
- userRow: {
- alignItems: 'center',
- marginLeft: Kb.Styles.globalMargins.xsmall,
- minHeight: 40,
- width: '100%',
- },
- }) as const
-)
-
-export default Login
diff --git a/shared/login/relogin/index.native.tsx b/shared/login/relogin/index.native.tsx
deleted file mode 100644
index 44cc9ffd1878..000000000000
--- a/shared/login/relogin/index.native.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import * as C from '@/constants'
-import * as Kb from '@/common-adapters'
-import {isAndroidNewerThanM} from '@/constants/platform'
-import * as React from 'react'
-import Dropdown from './dropdown.native'
-import UserCard from '../user-card'
-import type {Input3Props} from '@/common-adapters/input3.shared'
-import type {Props} from './index.shared'
-
-const LoginRender = (props: Props) => {
- const [scrollViewHeight, setScrollViewHeight] = React.useState(undefined)
- const inputProps: Input3Props = {
- autoFocus: true,
- error: !!props.error,
- keyboardType: props.showTyping && isAndroid ? 'visible-password' : 'default',
- onChangeText: password => props.passwordChange(password),
- onEnterKeyDown: () => props.onSubmit(),
- placeholder: 'Password',
- secureTextEntry: !props.showTyping,
- }
-
- return (
- setScrollViewHeight(evt.nativeEvent.layout.height)}
- style={Kb.Styles.globalStyles.flexOne}
- >
-
-
- {isAndroid && !C.isDeviceSecureAndroid && !isAndroidNewerThanM && (
-
-
- {"Since you don't have a lock screen, you'll have to type your password everytime."}
-
-
- )}
- {!!props.error && {props.error}}
-
-
- {props.needPassword && (
-
-
- props.showTypingChange(check)}
- style={styles.formElements}
- />
-
- )}
-
-
- Forgot password?
-
-
- Problems logging in?
-
-
-
-
-
-
-
-
-
- )
-}
-
-const styles = Kb.Styles.styleSheetCreate(
- () =>
- ({
- card: {
- marginTop: Kb.Styles.globalMargins.medium,
- width: '100%',
- },
- cardInner: Kb.Styles.platformStyles({
- isTablet: {paddingBottom: 0},
- }),
- container: {
- backgroundColor: Kb.Styles.globalColors.blueGrey,
- },
- createAccountContainer: Kb.Styles.platformStyles({
- common: {padding: Kb.Styles.globalMargins.medium},
- isTablet: {maxWidth: 410, padding: Kb.Styles.globalMargins.small},
- }),
- deviceNotSecureContainer: {
- alignSelf: 'stretch',
- backgroundColor: Kb.Styles.globalColors.yellow,
- paddingBottom: Kb.Styles.globalMargins.tiny,
- paddingTop: Kb.Styles.globalMargins.tiny,
- },
- deviceNotSecureText: {
- color: Kb.Styles.globalColors.brown_75,
- },
- formElements: {
- marginBottom: Kb.Styles.globalMargins.tiny,
- },
- scrollView: {
- backgroundColor: Kb.Styles.globalColors.blueGrey,
- },
- }) as const
-)
-
-export default LoginRender
diff --git a/shared/login/relogin/index.tsx b/shared/login/relogin/index.tsx
new file mode 100644
index 000000000000..42ca89309653
--- /dev/null
+++ b/shared/login/relogin/index.tsx
@@ -0,0 +1,285 @@
+import * as C from '@/constants'
+import * as React from 'react'
+import * as Kb from '@/common-adapters'
+import UserCard from '../user-card'
+import {errorBanner, SignupScreen} from '@/signup/common'
+import {isAndroidNewerThanM} from '@/constants/platform'
+import Dropdown from './dropdown.native'
+import type {Props} from './index.shared'
+
+// Desktop login
+
+const other = 'Someone else...'
+
+const UserRow = ({user, hasStoredSecret}: {user: string; hasStoredSecret: boolean}) => (
+
+
+ {user}
+
+ {hasStoredSecret && • Signed in}
+
+)
+
+const DesktopLogin = (props: Props) => {
+ const _inputRef = React.useRef(null)
+
+ const _onClickUserIdx = (selected: number) => {
+ const user = props.users.at(selected)
+ if (!user) {
+ props.onSomeoneElse()
+ } else {
+ props.selectedUserChange(user.username)
+ if (_inputRef.current) {
+ _inputRef.current.focus()
+ }
+ }
+ }
+
+ const userRows = props.users
+ .concat({hasStoredSecret: false, uid: '', username: other})
+ .map(u => )
+
+ const selectedIdx = props.users.findIndex(u => u.username === props.selectedUser)
+ return (
+
+
+
+
+ {props.needPassword && (
+
+
+
+ )}
+
+
+ Forgot password?
+
+
+
+
+
+
+
+
+ )
+}
+
+const desktopStyles = Kb.Styles.styleSheetCreate(
+ () =>
+ ({
+ container: {
+ ...Kb.Styles.globalStyles.flexBoxColumn,
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ },
+ contentBox: {
+ alignSelf: 'center',
+ maxWidth: 460,
+ padding: Kb.Styles.globalMargins.small,
+ },
+ forgotPassword: {
+ marginTop: Kb.Styles.globalMargins.tiny,
+ },
+ header: {
+ borderBottomWidth: 0,
+ },
+ inputRow: {
+ marginBottom: 0,
+ marginTop: Kb.Styles.globalMargins.tiny,
+ width: '100%',
+ },
+ loginSubmitButton: {
+ marginTop: 0,
+ maxHeight: 32,
+ width: '100%',
+ },
+ other: {color: Kb.Styles.globalColors.black},
+ provisioned: {color: Kb.Styles.globalColors.orange},
+ userContainer: {
+ backgroundColor: Kb.Styles.globalColors.transparent,
+ flex: 1,
+ },
+ userDropdown: {
+ backgroundColor: Kb.Styles.globalColors.white,
+ width: '100%',
+ },
+ userOverlayStyle: {
+ backgroundColor: Kb.Styles.globalColors.white,
+ width: 348,
+ },
+ userRow: {
+ alignItems: 'center',
+ marginLeft: Kb.Styles.globalMargins.xsmall,
+ minHeight: 40,
+ width: '100%',
+ },
+ }) as const
+)
+
+// Native login
+
+const NativeLoginRender = (props: Props) => {
+ const [scrollViewHeight, setScrollViewHeight] = React.useState(undefined)
+ const inputProps = {
+ autoFocus: true,
+ error: !!props.error,
+ keyboardType: props.showTyping && isAndroid ? 'visible-password' : 'default',
+ onChangeText: (password: string) => props.passwordChange(password),
+ onEnterKeyDown: () => props.onSubmit(),
+ placeholder: 'Password',
+ secureTextEntry: !props.showTyping,
+ } as const
+
+ return (
+ setScrollViewHeight(evt.nativeEvent.layout.height)}
+ style={Kb.Styles.globalStyles.flexOne}
+ >
+
+
+ {isAndroid && !C.isDeviceSecureAndroid && !isAndroidNewerThanM && (
+
+
+ {"Since you don't have a lock screen, you'll have to type your password everytime."}
+
+
+ )}
+ {!!props.error && {props.error}}
+
+
+ {props.needPassword && (
+
+
+ props.showTypingChange(check)}
+ style={nativeStyles.formElements}
+ />
+
+ )}
+
+
+ Forgot password?
+
+
+ Problems logging in?
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const nativeStyles = Kb.Styles.styleSheetCreate(
+ () =>
+ ({
+ card: {
+ marginTop: Kb.Styles.globalMargins.medium,
+ width: '100%',
+ },
+ cardInner: Kb.Styles.platformStyles({
+ isTablet: {paddingBottom: 0},
+ }),
+ container: {
+ backgroundColor: Kb.Styles.globalColors.blueGrey,
+ },
+ createAccountContainer: Kb.Styles.platformStyles({
+ common: {padding: Kb.Styles.globalMargins.medium},
+ isTablet: {maxWidth: 410, padding: Kb.Styles.globalMargins.small},
+ }),
+ deviceNotSecureContainer: {
+ alignSelf: 'stretch',
+ backgroundColor: Kb.Styles.globalColors.yellow,
+ paddingBottom: Kb.Styles.globalMargins.tiny,
+ paddingTop: Kb.Styles.globalMargins.tiny,
+ },
+ deviceNotSecureText: {
+ color: Kb.Styles.globalColors.brown_75,
+ },
+ formElements: {
+ marginBottom: Kb.Styles.globalMargins.tiny,
+ },
+ scrollView: {
+ backgroundColor: Kb.Styles.globalColors.blueGrey,
+ },
+ }) as const
+)
+
+export default isMobile ? NativeLoginRender : DesktopLogin
diff --git a/shared/menubar/index.desktop.tsx b/shared/menubar/index.desktop.tsx
index 208f3ad595af..c9a8e143d3d5 100644
--- a/shared/menubar/index.desktop.tsx
+++ b/shared/menubar/index.desktop.tsx
@@ -7,7 +7,7 @@ import * as RemoteGen from '@/constants/remote-actions'
import * as FsUtil from '@/util/kbfs'
import * as TimestampUtil from '@/util/timestamp'
import Filename from '@/fs/common/filename'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
import OutOfDate from './out-of-date'
import Upload from '@/fs/footer/upload'
import {openURL as openUrl} from '@/util/misc'
diff --git a/shared/menubar/main.desktop.tsx b/shared/menubar/main.desktop.tsx
index 06f1bc3b7d9c..aac114683bf5 100644
--- a/shared/menubar/main.desktop.tsx
+++ b/shared/menubar/main.desktop.tsx
@@ -1,6 +1,6 @@
// Entry point for the menubar render window
import '../desktop/renderer/globals.desktop'
-import {waitOnKB2Loaded} from '@/util/electron.desktop'
+import {waitOnKB2Loaded} from '@/util/electron'
waitOnKB2Loaded(() => {
import('./main2.desktop').then(() => {}).catch(() => {})
})
diff --git a/shared/menubar/remote-proxy.desktop.tsx b/shared/menubar/remote-proxy.desktop.tsx
index e9322d5bcca3..93f7d2006d72 100644
--- a/shared/menubar/remote-proxy.desktop.tsx
+++ b/shared/menubar/remote-proxy.desktop.tsx
@@ -5,7 +5,7 @@ import {ensureWidgetMetas, useInboxMetadataState} from '@/chat/inbox/metadata'
import {useConfigState} from '@/stores/config'
import * as T from '@/constants/types'
import * as React from 'react'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
import useSerializeProps from '../desktop/remote/use-serialize-props.desktop'
import type {Props, Conversation, RemoteTlfUpdates} from './index.desktop'
import {useColorScheme} from 'react-native'
diff --git a/shared/native-only-modules.js b/shared/native-only-modules.js
index 28beb3d716dc..c765646aefaa 100644
--- a/shared/native-only-modules.js
+++ b/shared/native-only-modules.js
@@ -19,4 +19,13 @@ module.exports = [
'expo-modules-core',
'react-native-kb',
'@react-native-picker/picker',
+ 'expo-clipboard',
+ 'expo-image',
+ 'expo-contacts',
+ 'expo-localization',
+ 'expo-media-library',
+ 'expo-file-system',
+ '@callstack/liquid-glass',
+ 'react-native-screens/experimental',
+ '@react-navigation/bottom-tabs',
]
diff --git a/shared/people/container.tsx b/shared/people/container.tsx
index 22793ad088b3..d1854766d417 100644
--- a/shared/people/container.tsx
+++ b/shared/people/container.tsx
@@ -125,8 +125,8 @@ const descriptionForTodoItem = (todo: T.RPCGen.HomeScreenTodo) => {
case t.verifyAllEmail:
return `Your email address *${todo.verifyAllEmail}* is unverified.`
case t.verifyAllPhoneNumber: {
- const {e164ToDisplay} = require('@/util/phone-numbers') as {e164ToDisplay: typeof e164ToDisplayType}
const p = todo.verifyAllPhoneNumber
+ const {e164ToDisplay} = require('@/util/phone-numbers') as {e164ToDisplay: typeof e164ToDisplayType}
return `Your number *${p ? e164ToDisplay(p) : ''}* is unverified.`
}
default: {
diff --git a/shared/pinentry/main.desktop.tsx b/shared/pinentry/main.desktop.tsx
index 6e63c296e84f..946161bd056c 100644
--- a/shared/pinentry/main.desktop.tsx
+++ b/shared/pinentry/main.desktop.tsx
@@ -1,4 +1,4 @@
// Entry point for the pinentry render window
import '../desktop/renderer/globals.desktop'
-import {waitOnKB2Loaded} from '@/util/electron.desktop'
+import {waitOnKB2Loaded} from '@/util/electron'
waitOnKB2Loaded(() => require('./main2.desktop') as () => void)
diff --git a/shared/profile/edit-avatar/index.tsx b/shared/profile/edit-avatar/index.tsx
index 8a4721130531..02fd265a9580 100644
--- a/shared/profile/edit-avatar/index.tsx
+++ b/shared/profile/edit-avatar/index.tsx
@@ -7,18 +7,11 @@ import {useSafeNavigation} from '@/util/safe-navigation'
import {ModalTitle} from '@/teams/common'
import useHooks from './hooks'
import './edit-avatar.css'
+import KB2 from '@/util/electron'
+import {launchImageLibraryAsync} from '@/util/expo-image-picker'
+import {CropZoom, type CropZoomRefType} from 'react-native-zoom-toolkit'
-// Desktop-only helpers loaded conditionally at module level (no runtime error on mobile since
-// these values are only referenced inside the desktop branch)
-const KB2 = !isMobile
- ? (require('@/util/electron.desktop').default as {
- functions: {
- isDirectory?: (path: string) => Promise
- getPathForFile?: (file: {name?: string; size?: number}) => string
- }
- })
- : undefined
-const desktopFns = KB2?.functions ?? {}
+const desktopFns = isMobile ? ({} as typeof KB2.functions) : KB2.functions
const AVATAR_CONTAINER_SIZE = 300
@@ -41,7 +34,7 @@ const getFile = async (fileList: FileListLike | undefined): Promise => {
if (!file) {
return ''
}
- const path = getPathForFile?.(file) ?? ''
+ const path = getPathForFile?.(file as unknown as File) ?? ''
if (!path) {
return ''
}
@@ -233,12 +226,6 @@ const NativeAvatarUploadWrapper = (p: Props) => {
const onChooseNewAvatar = () => {
const f = async () => {
try {
- const {launchImageLibraryAsync} = require('@/util/expo-image-picker') as {
- launchImageLibraryAsync: (type: string) => Promise<{
- canceled: boolean
- assets?: Array<{uri: string; width: number; height: number; type?: string}>
- }>
- }
const result = await launchImageLibraryAsync('photo')
const first = result.assets?.reduce((acc, a) => {
if (!acc && (a.type === 'image' || a.type === 'video')) {
@@ -263,12 +250,6 @@ const NativeAvatarUploadWrapper = (p: Props) => {
if (!wizard && noImage) {
const f = async () => {
try {
- const {launchImageLibraryAsync} = require('@/util/expo-image-picker') as {
- launchImageLibraryAsync: (type: string) => Promise<{
- canceled: boolean
- assets?: Array<{uri: string; width: number; height: number; type?: string}>
- }>
- }
const result = await launchImageLibraryAsync('photo')
const first = result.assets?.reduce((acc, a) => {
if (!acc && (a.type === 'image' || a.type === 'video')) {
@@ -423,19 +404,6 @@ function NativeAvatarZoom(p: {src?: string; width: number; height: number; ref?:
const {src, width, height, ref} = p
const resolution = {height, width}
- // Load CropZoom lazily (native only)
- const {CropZoom, type: _unused} = require('react-native-zoom-toolkit') as {
- CropZoom: React.ComponentType<{
- cropSize: {width: number; height: number}
- resolution: {width: number; height: number}
- ref?: React.Ref<{crop: (size: number) => {resize?: {width: number}; crop: {originX: number; originY: number; width: number; height: number}}}>
- panMode?: string
- minScale?: number
- children?: React.ReactNode
- }>
- type: unknown
- }
- type CropZoomRefType = {crop: (size: number) => {resize?: {width: number}; crop: {originX: number; originY: number; width: number; height: number}}}
React.useImperativeHandle(ref, () => {
return {
diff --git a/shared/provision/code-page/qr-scan/scanner.desktop.tsx b/shared/provision/code-page/qr-scan/scanner.desktop.tsx
deleted file mode 100644
index c94238fbd2e0..000000000000
--- a/shared/provision/code-page/qr-scan/scanner.desktop.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type * as React from 'react'
-import type * as Kb from '@/common-adapters'
-
-type Props = {
- onBarCodeRead: (code: string) => void
- notAuthorizedView: React.ReactElement | null
- style: Kb.Styles.StylesCrossPlatform
-}
-
-const QRScanner = (_p: Props): null => null
-export default QRScanner
diff --git a/shared/provision/code-page/qr-scan/scanner.native.tsx b/shared/provision/code-page/qr-scan/scanner.tsx
similarity index 90%
rename from shared/provision/code-page/qr-scan/scanner.native.tsx
rename to shared/provision/code-page/qr-scan/scanner.tsx
index 2e3eaa821302..e22a9b95f37c 100644
--- a/shared/provision/code-page/qr-scan/scanner.native.tsx
+++ b/shared/provision/code-page/qr-scan/scanner.tsx
@@ -8,7 +8,7 @@ type Props = {
style: Kb.Styles.StylesCrossPlatform
}
-const QRScanner = (p: Props): React.ReactElement | null => {
+const QRScannerMobile = (p: Props): React.ReactElement | null => {
const [scanned, setScanned] = React.useState(false)
const [permission, requestPermission] = useCameraPermissions()
@@ -48,4 +48,9 @@ const styles = Kb.Styles.styleSheetCreate(() => ({
},
}))
+const QRScanner = (p: Props): React.ReactElement | null => {
+ if (!isMobile) return null
+ return
+}
+
export default QRScanner
diff --git a/shared/router-v2/common.tsx b/shared/router-v2/common.tsx
index 59153e191d31..6b331bdc0f00 100644
--- a/shared/router-v2/common.tsx
+++ b/shared/router-v2/common.tsx
@@ -1,4 +1,4 @@
-import * as React from 'react'
+import type * as React from 'react'
import * as Kb from '@/common-adapters'
import {TabActions, type NavigationContainerRef} from '@react-navigation/core'
import type {ParamListBase} from '@react-navigation/native'
diff --git a/shared/router-v2/header/index.desktop.tsx b/shared/router-v2/header/index.desktop.tsx
index 49e68779ab79..9e9303b93a1b 100644
--- a/shared/router-v2/header/index.desktop.tsx
+++ b/shared/router-v2/header/index.desktop.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import * as Kb from '@/common-adapters'
import * as Platform from '@/constants/platform'
import SyncingFolders from './syncing-folders'
-import KB2 from '@/util/electron.desktop'
+import KB2 from '@/util/electron'
import {useConfigState} from '@/stores/config'
import {useShellState} from '@/stores/shell'
import type {HeaderBackButtonProps} from '@react-navigation/elements'
diff --git a/shared/router-v2/linking.tsx b/shared/router-v2/linking.tsx
index 5043bb3e6266..e33aab16ff99 100644
--- a/shared/router-v2/linking.tsx
+++ b/shared/router-v2/linking.tsx
@@ -5,6 +5,7 @@ import {useConfigState} from '@/stores/config'
import type * as UsePushStateType from '@/stores/push'
import type {LinkingOptions} from '@react-navigation/native'
import type {RootParamList} from './route-params'
+import {Linking} from 'react-native'
// ---- URL normalization ----
@@ -254,9 +255,7 @@ export const createLinkingConfig = (
let deepLinkUrl: string | null = null
if (isMobile) {
try {
-
- const RN: {Linking: {getInitialURL: () => Promise}} = require('react-native')
- deepLinkUrl = await RN.Linking.getInitialURL()
+ deepLinkUrl = await Linking.getInitialURL()
} catch {}
}
@@ -342,27 +341,22 @@ export const createLinkingConfig = (
// On native, listen for RN Linking 'url' events (warm-start deep links)
let removeLinkingSub: (() => void) | undefined
if (isMobile) {
- try {
-
- const RN: {Linking: {addEventListener: (type: string, handler: (e: {url: string}) => void) => {remove: () => void}}} = require('react-native')
- const {Linking} = RN
- const sub = Linking.addEventListener('url', ({url}: {url: string}) => {
- const normalized = normalizeUrl(url)
- if (!normalized) return
- // Profile deep links need imperative navigation to properly set up
- // the back stack. State-based linking may not create intermediate screens.
- if (normalized.startsWith('keybase://profile/')) {
- handleAppLink(normalized)
- return
- }
- if (isHandledByLinkingConfig(normalized)) {
- dedupedListener(normalized)
- } else {
- handleAppLink(normalized)
- }
- })
- removeLinkingSub = () => sub.remove()
- } catch {}
+ const sub = Linking.addEventListener('url', ({url}: {url: string}) => {
+ const normalized = normalizeUrl(url)
+ if (!normalized) return
+ // Profile deep links need imperative navigation to properly set up
+ // the back stack. State-based linking may not create intermediate screens.
+ if (normalized.startsWith('keybase://profile/')) {
+ handleAppLink(normalized)
+ return
+ }
+ if (isHandledByLinkingConfig(normalized)) {
+ dedupedListener(normalized)
+ } else {
+ handleAppLink(normalized)
+ }
+ })
+ removeLinkingSub = () => sub.remove()
}
return () => {
diff --git a/shared/router-v2/router.desktop.tsx b/shared/router-v2/router.desktop.tsx
deleted file mode 100644
index 47a50b949fd7..000000000000
--- a/shared/router-v2/router.desktop.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import * as Common from './common'
-import * as C from '@/constants'
-import {useConfigState} from '@/stores/config'
-import {useDarkModeState} from '@/stores/darkmode'
-import * as Kb from '@/common-adapters'
-import * as React from 'react'
-import * as Shared from './router.shared'
-import * as Tabs from '@/constants/tabs'
-import logger from '@/logger'
-import Header from './header/index.desktop'
-import {HeaderLeftButton} from '@/common-adapters/header-buttons'
-import {NavigationContainer} from '@react-navigation/native'
-import {createLeftTabNavigator} from './left-tab-navigator.desktop'
-import {createLinkingConfig} from './linking'
-import {handleAppLink} from '@/constants/deeplinks'
-import {modalRoutes, routes, loggedOutRoutes, tabRoots, routeMapToStaticScreens} from './routes'
-import {registerDebugClear} from '@/util/debug'
-import {useDaemonState} from '@/stores/daemon'
-import {useCurrentUserState} from '@/stores/current-user'
-import {createNativeStackNavigator} from '@react-navigation/native-stack'
-import {LoadedTeamsListProvider} from '@/teams/use-teams-list'
-
-import type {NativeStackNavigationOptions} from '@react-navigation/native-stack'
-import {makeLayout} from './screen-layout.desktop'
-import './router.css'
-
-const Tab = createLeftTabNavigator()
-
-const appTabsInnerOptions = {
- ...Common.defaultNavigationOptions,
- header: undefined,
- headerShown: false,
- tabBarActiveBackgroundColor: Kb.Styles.globalColors.blueDarkOrGreyDarkest,
- tabBarHideOnKeyboard: true,
- tabBarInactiveBackgroundColor: Kb.Styles.globalColors.blueDarkOrGreyDarkest,
- tabBarShowLabel: Kb.Styles.isTablet,
- tabBarStyle: Common.tabBarStyle,
-}
-
-const tabScreensConfig = routeMapToStaticScreens(routes, makeLayout, false, false, true)
-
-const tabComponents: Record = {}
-for (const tab of Tabs.desktopTabs) {
- const nav = createNativeStackNavigator({
- initialRouteName: tabRoots[tab],
- screenOptions: Common.defaultNavigationOptions as NativeStackNavigationOptions,
- screens: tabScreensConfig,
- })
- tabComponents[tab] = nav.getComponent()
-}
-
-function AppTabsInner() {
- return (
-
- {Tabs.desktopTabs.map(tab => (
-
- ))}
-
- )
-}
-
-const AppTabs = () =>
-
-const loggedOutScreensConfig = routeMapToStaticScreens(loggedOutRoutes, makeLayout, false, true, false)
-const loggedOutOptions = {
- header: p => {
- const options: React.ComponentProps['options'] = {
- ...(p.options as React.ComponentProps['options']),
- headerBottomStyle: {height: 0},
- headerShadowVisible: false,
- }
- return
- },
-} satisfies NativeStackNavigationOptions
-const loggedOutNav = createNativeStackNavigator({
- initialRouteName: 'login',
- screenOptions: loggedOutOptions,
- screens: loggedOutScreensConfig,
-})
-const LoggedOut = loggedOutNav.getComponent()
-
-const documentTitle = {
- formatter: () => {
- const t = C.Router2.getTab()
- const m = t ? C.Tabs.desktopTabMeta[t] : undefined
- const tabLabel: string = m?.label ?? ''
- return `Keybase: ${tabLabel}`
- },
-}
-
-const rootScreenOptions = {
- headerLeft: () => ,
- headerShown: false, // eventually do this after we pull apart modal2 etc
- presentation: 'transparentModal' as const,
- title: '',
-} satisfies NativeStackNavigationOptions
-
-const useConnectNavToState = () => {
- const setNavOnce = React.useRef(false)
- React.useEffect(() => {
- if (!setNavOnce.current) {
- if (C.Router2.navigationRef.isReady()) {
- setNavOnce.current = true
-
- if (__DEV__) {
- window.DEBUGNavigator = C.Router2.navigationRef.current
- window.DEBUGRouter2 = C.Router2
- window.KBCONSTANTS = require('@/constants')
- window.KBINBOX = require('@/constants/chat')
- registerDebugClear(() => {
- window.DEBUGNavigator = undefined
- window.DEBUGRouter2 = undefined
- window.KBCONSTANTS = undefined
- window.KBINBOX = undefined
- })
- }
- }
- }
- }, [setNavOnce])
-}
-
-// Set up the fallback handler for emitDeepLink on desktop (no linking prop needed on Electron)
-createLinkingConfig(handleAppLink)
-
-const useIsLoading = () => {
- const everLoadedRef = React.useRef(false)
- return !useDaemonState(s => {
- const loaded = everLoadedRef.current || s.handshakeState === 'done'
- everLoadedRef.current = loaded
- return loaded
- })
-}
-
-const useIsLoggedIn = () => {
- const everLoadedRef = React.useRef(false)
- const loggedInLoaded = useDaemonState(s => {
- const loaded = everLoadedRef.current || s.handshakeState === 'done'
- everLoadedRef.current = loaded
- return loaded
- })
- const loggedIn = useConfigState(s => s.loggedIn)
- return loggedInLoaded && loggedIn
-}
-
-const useIsLoggedOut = () => {
- const everLoadedRef = React.useRef(false)
- const loggedInLoaded = useDaemonState(s => {
- const loaded = everLoadedRef.current || s.handshakeState === 'done'
- everLoadedRef.current = loaded
- return loaded
- })
- const loggedIn = useConfigState(s => s.loggedIn)
- return loggedInLoaded && !loggedIn
-}
-
-const modalScreensConfig = routeMapToStaticScreens(modalRoutes, makeLayout, true, false, false)
-
-const rootNav = createNativeStackNavigator({
- groups: {
- loggedIn: {
- if: useIsLoggedIn,
- screens: {
- loggedIn: {screen: AppTabs},
- ...modalScreensConfig,
- },
- },
- loggedOut: {
- if: useIsLoggedOut,
- screens: {
- loggedOut: {screen: LoggedOut},
- },
- },
- },
- screenOptions: rootScreenOptions,
- screens: {
- loading: {
- if: useIsLoading,
- screen: Shared.SimpleLoading,
- },
- },
-})
-const RootComponent = rootNav.getComponent()
-
-function ElectronApp() {
- useConnectNavToState()
-
- const onUnhandledAction = (a: Readonly<{type: string}>) => {
- logger.info(`[NAV] Unhandled action: ${a.type}`, a, C.Router2.logState())
- }
-
- const setNavState = C.useRouterState(s => s.dispatch.setNavState)
- const onStateChange = () => {
- const ns = C.Router2.getRootState()
- setNavState(ns)
- }
-
- const navRef = (ref: typeof C.Router2.navigationRef.current) => {
- if (ref) {
- C.Router2.navigationRef.current = ref
- }
- }
-
- const isDarkMode = useDarkModeState(s => s.isDarkMode())
- const username = useCurrentUserState(s => s.username)
- // Only remount the navigator when switching between logged-in users.
- // Ignore '' → username (initial login) so in-flight unbox requests aren't interrupted.
- const [navKey, setNavKey] = React.useState('')
- const prevUsernameRef = React.useRef(username)
- React.useEffect(() => {
- const prev = prevUsernameRef.current
- prevUsernameRef.current = username
- if (prev && username && prev !== username) {
- setNavKey(username)
- }
- }, [username])
-
- return (
-
-
-
-
-
- )
-}
-
-export default ElectronApp
diff --git a/shared/router-v2/router.native.tsx b/shared/router-v2/router.native.tsx
deleted file mode 100644
index 8013d427b742..000000000000
--- a/shared/router-v2/router.native.tsx
+++ /dev/null
@@ -1,353 +0,0 @@
-///
-import * as C from '@/constants'
-import * as Constants from '@/constants/router'
-import {useConfigState} from '@/stores/config'
-import {useDarkModeState} from '@/stores/darkmode'
-import * as Kb from '@/common-adapters'
-import * as React from 'react'
-import * as Shared from './router.shared'
-import * as Tabs from '@/constants/tabs'
-import * as Common from './common'
-import logger from '@/logger'
-import {Platform, StatusBar, View} from 'react-native'
-import {HeaderLeftButton} from '@/common-adapters/header-buttons'
-import {NavigationContainer, type NavigationProp} from '@react-navigation/native'
-import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
-import {modalRoutes, routes, loggedOutRoutes, tabRoots, routeMapToStaticScreens} from './routes'
-import {createNativeStackNavigator} from '@react-navigation/native-stack'
-import {isLiquidGlassSupported as _isLiquidGlassSupported} from '@callstack/liquid-glass'
-import type {NativeStackNavigationOptions} from '@react-navigation/native-stack'
-import type {SFSymbol} from 'sf-symbols-typescript'
-import {makeLayout} from './screen-layout.native'
-import {useRootKey} from './hooks.native'
-import {createLinkingConfig} from './linking'
-import type {RootParamList} from './route-params'
-import {handleAppLink} from '@/constants/deeplinks'
-import {useDaemonState} from '@/stores/daemon'
-import {useNotifState} from '@/stores/notifications'
-import {usePushState} from '@/stores/push'
-import {LoadedTeamsListProvider} from '@/teams/use-teams-list'
-import {colors} from '@/styles/colors'
-
-const isLiquidGlassSupported = _isLiquidGlassSupported as boolean
-
-if (module.hot) {
- module.hot.accept('', () => {})
-}
-
-const tabToLabel = new Map([
- [Tabs.chatTab, 'Chat'],
- [Tabs.fsTab, 'Files'],
- [Tabs.teamsTab, 'Teams'],
- [Tabs.peopleTab, 'People'],
- [Tabs.settingsTab, 'More'],
-])
-
-// just to get badge rollups
-const tabs = C.isTablet ? Tabs.tabletTabs : Tabs.phoneTabs
-
-const Tab = createBottomTabNavigator()
-const tabRoutes = routes
-const settingsTabChildren = [Tabs.gitTab, Tabs.devicesTab, Tabs.settingsTab] as const
-
-const tabStackOptions = ({
- navigation,
-}: {
- navigation: {canGoBack: () => boolean}
-}): NativeStackNavigationOptions => ({
- ...Common.defaultNavigationOptions,
- // Use the native back button (liquid glass pill on iOS 26) for non-root screens;
- // omit headerLeft entirely on root screens so no empty glass circle appears.
- headerBackVisible: navigation.canGoBack(),
- headerLeft: undefined,
-})
-
-// On phones, each tab stack only contains its root screen. All other routes live in
-// the root stack (alongside chatConversation) so they render above the tab bar.
-const tabRootNameSet = new Set(Object.values(tabRoots).filter(Boolean))
-const phoneRootRoutes = Object.fromEntries(
- Object.entries(tabRoutes).filter(([name]) => !tabRootNameSet.has(name))
-) as typeof tabRoutes
-
-const tabScreensConfig = routeMapToStaticScreens(tabRoutes, makeLayout, false, false, true)
-const phoneRootScreensConfig = routeMapToStaticScreens(
- C.isTablet ? {} : phoneRootRoutes,
- makeLayout,
- false,
- false,
- false
-)
-
-const tabComponents: Record = {}
-for (const tab of tabs) {
- if (C.isTablet) {
- const nav = createNativeStackNavigator({
- initialRouteName: tabRoots[tab],
- screenOptions: tabStackOptions,
- screens: tabScreensConfig,
- })
- tabComponents[tab] = nav.getComponent()
- } else {
- const rootName = tabRoots[tab]
- const rootScreenConfig = routeMapToStaticScreens(
- {[rootName]: tabRoutes[rootName as keyof typeof tabRoutes]} as typeof tabRoutes,
- makeLayout,
- false,
- false,
- true
- )
- const nav = createNativeStackNavigator({
- initialRouteName: rootName,
- screenOptions: tabStackOptions,
- screens: rootScreenConfig,
- })
- tabComponents[tab] = nav.getComponent()
- }
-}
-
-const androidTabIcons = new Map([
- [Tabs.chatTab, require('../images/icons/icon-nav-chat-32.png')],
- [Tabs.fsTab, require('../images/icons/icon-nav-folders-32.png')],
- [Tabs.peopleTab, require('../images/icons/icon-nav-people-32.png')],
- [Tabs.settingsTab, require('../images/icons/icon-nav-settings-32.png')],
- [Tabs.teamsTab, require('../images/icons/icon-nav-teams-32.png')],
-])
-
-const iosTabIcons = new Map([
- [Tabs.chatTab, {active: 'bubble.fill', inactive: 'bubble'}],
- [Tabs.fsTab, {active: 'folder.fill', inactive: 'folder'}],
- [Tabs.peopleTab, {active: 'person.crop.rectangle.fill', inactive: 'person.crop.rectangle'}],
- [Tabs.settingsTab, {active: 'line.3.horizontal.circle.fill', inactive: 'line.3.horizontal'}],
- [Tabs.teamsTab, {active: 'person.2.fill', inactive: 'person.2'}],
-])
-
-const getNativeTabIcon = (tab: Tabs.Tab) => {
- if (Platform.OS === 'ios') {
- const icon = iosTabIcons.get(tab)
- return icon
- ? ({focused}: {focused: boolean}) => ({
- name: focused ? icon.active : icon.inactive,
- type: 'sfSymbol' as const,
- })
- : undefined
- }
- const source = androidTabIcons.get(tab)
- return source ? {source, type: 'image' as const} : undefined
-}
-
-const getBadgeNumber = (
- routeName: Tabs.Tab,
- navBadges: ReadonlyMap,
- hasPermissions: boolean
-) => {
- const onSettings = routeName === Tabs.settingsTab
- const tabsToCount: ReadonlyArray = onSettings ? settingsTabChildren : [routeName]
- const count = tabsToCount.reduce(
- (res, tab) => res + (navBadges.get(tab) || 0),
- onSettings && !hasPermissions ? 1 : 0
- )
- return count || undefined
-}
-
-const appTabsScreenOptions = (
- routeName: Tabs.Tab,
- navBadges: ReadonlyMap,
- hasPermissions: boolean,
- isDarkMode: boolean
-) => {
- return {
- headerShown: false,
- overrideScrollViewContentInsetAdjustmentBehavior: true,
- tabBarBadge: getBadgeNumber(routeName, navBadges, hasPermissions),
- tabBarBadgeStyle: {
- backgroundColor: isLiquidGlassSupported ? Kb.Styles.globalColors.blue : Kb.Styles.globalColors.orange,
- },
- ...(isIOS
- ? {
- tabBarActiveIndicatorEnabled: false,
- ...(isLiquidGlassSupported
- ? {
- tabBarBlurEffect: Common.tabBarBlurEffect,
- }
- : {
- tabBarActiveTintColor: Kb.Styles.globalColors.whiteOrWhite,
- tabBarInactiveTintColor: isDarkMode ? colors.black : colors.blueDarker,
- tabBarMinimizeBehavior: Common.tabBarMinimizeBehavior,
- }),
- }
- : {
- tabBarActiveIndicatorColor: 'rgba(255,255,255,0.15)',
- tabBarActiveIndicatorEnabled: true,
- tabBarActiveTintColor: Kb.Styles.globalColors.white,
- tabBarInactiveTintColor: Kb.Styles.globalColors.blueLighter,
- }),
- tabBarIcon: getNativeTabIcon(routeName),
- tabBarLabel: tabToLabel.get(routeName) ?? routeName,
- tabBarLabelVisibilityMode: 'labeled' as const,
- tabBarMinimizeBehavior: 'none' as const, // until this actually works on all screens, not sure why it only
- tabBarStyle: {backgroundColor: isDarkMode ? colors.greyDarkest : colors.blueDark},
- // works on chat inbox now
- title: tabToLabel.get(routeName) ?? routeName,
- }
-}
-function AppTabs() {
- const navBadges = useNotifState(s => s.navBadges)
- const hasPermissions = usePushState(s => s.hasPermissions)
- const isDarkMode = useDarkModeState(s => s.isDarkMode())
-
- return (
-
- {tabs.map(tab => (
-
- ))}
-
- )
-}
-
-const loggedOutScreenOptions = {
- ...Common.defaultNavigationOptions,
-} as const
-const loggedOutScreensConfig = routeMapToStaticScreens(loggedOutRoutes, makeLayout, false, true, false)
-const loggedOutNav = createNativeStackNavigator({
- initialRouteName: 'login',
- screenOptions: loggedOutScreenOptions as NativeStackNavigationOptions,
- screens: loggedOutScreensConfig,
-})
-const LoggedOut = loggedOutNav.getComponent()
-
-const rootStackScreenOptions = {headerBackButtonDisplayMode: 'minimal'} satisfies NativeStackNavigationOptions
-const modalScreenOptions = ({
- navigation,
-}: {
- navigation: NavigationProp
-}): NativeStackNavigationOptions => {
- const cancelItem: NativeStackNavigationOptions =
- Platform.OS === 'ios'
- ? {
- unstable_headerLeftItems: () => [
- {label: 'Cancel', onPress: () => navigation.goBack(), type: 'button' as const},
- ],
- }
- : {headerLeft: () => }
- return {
- ...cancelItem,
- headerShown: true,
- presentation: 'modal',
- title: '',
- }
-}
-
-const useIsLoggedIn = () => useConfigState(s => s.loggedIn)
-const useIsLoggedOut = () => !useConfigState(s => s.loggedIn)
-
-const modalScreensConfig = routeMapToStaticScreens(modalRoutes, makeLayout, true, false, false)
-
-const rootNav = createNativeStackNavigator({
- groups: {
- loggedIn: {
- if: useIsLoggedIn,
- screens: {
- loggedIn: {options: {headerShown: false}, screen: AppTabs},
- ...phoneRootScreensConfig,
- },
- },
- loggedOut: {
- if: useIsLoggedOut,
- screens: {
- loggedOut: {options: {headerShown: false}, screen: LoggedOut},
- },
- },
- modals: {
- if: useIsLoggedIn,
- screenOptions: modalScreenOptions as NativeStackNavigationOptions,
- screens: modalScreensConfig,
- },
- },
- screenOptions: rootStackScreenOptions,
-})
-const RootComponent = rootNav.getComponent()
-
-// Create once, stable across renders. handleAppLink is used as fallback for
-// URL patterns not yet handled by the linking config.
-const linkingConfig = createLinkingConfig(handleAppLink)
-
-function RNApp() {
- const everLoadedRef = React.useRef(false)
- const loggedInLoaded = useDaemonState(s => {
- const loaded = everLoadedRef.current || s.handshakeState === 'done'
- everLoadedRef.current = loaded
- return loaded
- })
-
- const {loggedIn, startupLoaded} = useConfigState(
- C.useShallow(s => ({loggedIn: s.loggedIn, startupLoaded: s.startup.loaded}))
- )
- const setNavState = C.useRouterState(s => s.dispatch.setNavState)
- const onStateChange = () => {
- const ns = C.Router2.getRootState()
- setNavState(ns)
- }
- // Sync the initial state from the linking config into the router store.
- // onStateChange doesn't fire for the initial state, so this ensures
- // onRouteChanged runs and conversation data gets loaded on startup.
- const onReady = onStateChange
-
- const onUnhandledAction = (a: Readonly<{type: string}>) => {
- logger.info(`[NAV] Unhandled action: ${a.type}`, a, C.Router2.logState())
- }
-
- const navRef = (ref: typeof Constants.navigationRef.current) => {
- if (ref) {
- Constants.navigationRef.current = ref
- }
- }
-
- const {barStyle, isDarkMode} = useDarkModeState(
- C.useShallow(s => {
- const isDarkMode = s.isDarkMode()
- const barStyle =
- s.darkModePreference === 'system'
- ? ('default' as const)
- : isDarkMode
- ? ('light-content' as const)
- : ('dark-content' as const)
- return {barStyle, isDarkMode}
- })
- )
- const bar = barStyle === 'default' ? null :
- const rootKey = useRootKey()
-
- if (!loggedInLoaded || (loggedIn && !startupLoaded)) {
- return (
-
-
-
- )
- }
-
- return (
-
- {bar}
- }
- linking={loggedIn ? linkingConfig : undefined}
- onReady={onReady}
- onStateChange={onStateChange}
- onUnhandledAction={onUnhandledAction}
- ref={navRef}
- theme={isDarkMode ? Shared.darkTheme : Shared.lightTheme}
- >
-
-
-
-
-
- )
-}
-
-export default RNApp
diff --git a/shared/router-v2/router.tsx b/shared/router-v2/router.tsx
new file mode 100644
index 000000000000..acaf9e0b3767
--- /dev/null
+++ b/shared/router-v2/router.tsx
@@ -0,0 +1,635 @@
+///
+import * as C from '@/constants'
+import * as Common from './common'
+import {useConfigState} from '@/stores/config'
+import {useDarkModeState} from '@/stores/darkmode'
+import * as Kb from '@/common-adapters'
+import * as React from 'react'
+import * as Shared from './router.shared'
+import * as Tabs from '@/constants/tabs'
+import logger from '@/logger'
+import {HeaderLeftButton} from '@/common-adapters/header-buttons'
+import {NavigationContainer} from '@react-navigation/native'
+import {createLinkingConfig} from './linking'
+import {handleAppLink} from '@/constants/deeplinks'
+import {modalRoutes, routes, loggedOutRoutes, tabRoots, routeMapToStaticScreens} from './routes'
+import {useDaemonState} from '@/stores/daemon'
+import {LoadedTeamsListProvider} from '@/teams/use-teams-list'
+import {makeLayout} from './screen-layout'
+import {createNativeStackNavigator} from '@react-navigation/native-stack'
+import type {NativeStackNavigationOptions} from '@react-navigation/native-stack'
+import type {SFSymbol} from 'sf-symbols-typescript'
+import type {NavigationProp} from '@react-navigation/native'
+import type {RootParamList} from './route-params'
+import {useCurrentUserState} from '@/stores/current-user'
+import * as Constants from '@/constants/router'
+import {useNotifState} from '@/stores/notifications'
+import {usePushState} from '@/stores/push'
+import {colors} from '@/styles/colors'
+import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
+import {isLiquidGlassSupported as _isLiquidGlassSupported} from '@callstack/liquid-glass'
+import {StatusBar, View, useColorScheme} from 'react-native'
+const isLiquidGlassSupported = isMobile ? (_isLiquidGlassSupported as boolean) : false
+
+// ─── Desktop ──────────────────────────────────────────────────────────────────
+
+if (!isMobile) {
+ // Set up the fallback handler for emitDeepLink on desktop (no linking prop needed on Electron)
+ createLinkingConfig(handleAppLink)
+}
+
+// Inline structural type for the left-tab navigator (avoids importing from .desktop.tsx)
+type LeftTabNavigatorType = {
+ Navigator: React.ComponentType<{
+ backBehavior?: string
+ screenOptions?: object
+ children?: React.ReactNode
+ }>
+ Screen: React.ComponentType<{
+ name: string
+ component: React.ComponentType
+ key?: string
+ }>
+}
+
+let desktopTab: LeftTabNavigatorType | undefined
+const desktopTabComponents: Record = {}
+let DesktopRootComponent: React.ComponentType
+let LoggedOutDesktop: React.ComponentType
+
+if (!isMobile) {
+ const {createLeftTabNavigator} = require('./left-tab-navigator.desktop') as {
+ createLeftTabNavigator: () => LeftTabNavigatorType
+ }
+ desktopTab = createLeftTabNavigator()
+
+ const desktopTabScreensConfig = routeMapToStaticScreens(routes, makeLayout, false, false, true)
+
+ const appTabsInnerOptions = {
+ ...Common.defaultNavigationOptions,
+ header: undefined,
+ headerShown: false,
+ tabBarActiveBackgroundColor: Kb.Styles.globalColors.blueDarkOrGreyDarkest,
+ tabBarHideOnKeyboard: true,
+ tabBarInactiveBackgroundColor: Kb.Styles.globalColors.blueDarkOrGreyDarkest,
+ tabBarShowLabel: Kb.Styles.isTablet,
+ tabBarStyle: Common.tabBarStyle,
+ }
+
+ for (const tab of Tabs.desktopTabs) {
+ const nav = createNativeStackNavigator({
+ initialRouteName: tabRoots[tab],
+ screenOptions: Common.defaultNavigationOptions as NativeStackNavigationOptions,
+ screens: desktopTabScreensConfig,
+ })
+ desktopTabComponents[tab] = nav.getComponent()
+ }
+
+ // Keep appTabsInnerOptions stable (defined above before the loop)
+ const capturedOptions = appTabsInnerOptions
+ const capturedTab = desktopTab
+
+ function AppTabsInnerDesktop() {
+ return (
+
+ {Tabs.desktopTabs.map(tab => (
+
+ ))}
+
+ )
+ }
+ const AppTabsDesktop = () =>
+
+ type DesktopHeaderProps = Record & {options: Record}
+ const DesktopHeaderComponent = (
+ require('./header/index.desktop') as {default: React.ComponentType}
+ ).default
+
+ const desktopLoggedOutScreensConfig = routeMapToStaticScreens(loggedOutRoutes, makeLayout, false, true, false)
+ const desktopLoggedOutOptions = {
+ header: (p: Record) => {
+ const options = {
+ ...((p['options'] as Record | undefined) ?? {}),
+ headerBottomStyle: {height: 0},
+ headerShadowVisible: false,
+ }
+ return
+ },
+ } satisfies NativeStackNavigationOptions
+
+ const loggedOutNav = createNativeStackNavigator({
+ initialRouteName: 'login',
+ screenOptions: desktopLoggedOutOptions,
+ screens: desktopLoggedOutScreensConfig,
+ })
+ LoggedOutDesktop = loggedOutNav.getComponent()
+
+ const desktopRootScreenOptions = {
+ headerLeft: () => ,
+ headerShown: false, // eventually do this after we pull apart modal2 etc
+ presentation: 'transparentModal' as const,
+ title: '',
+ } satisfies NativeStackNavigationOptions
+
+ const useIsLoadingDesktop = () => {
+ const everLoadedRef = React.useRef(false)
+ return !useDaemonState(s => {
+ const loaded = everLoadedRef.current || s.handshakeState === 'done'
+ everLoadedRef.current = loaded
+ return loaded
+ })
+ }
+
+ const useIsLoggedInDesktop = () => {
+ const everLoadedRef = React.useRef(false)
+ const loggedInLoaded = useDaemonState(s => {
+ const loaded = everLoadedRef.current || s.handshakeState === 'done'
+ everLoadedRef.current = loaded
+ return loaded
+ })
+ const loggedIn = useConfigState(s => s.loggedIn)
+ return loggedInLoaded && loggedIn
+ }
+
+ const useIsLoggedOutDesktop = () => {
+ const everLoadedRef = React.useRef(false)
+ const loggedInLoaded = useDaemonState(s => {
+ const loaded = everLoadedRef.current || s.handshakeState === 'done'
+ everLoadedRef.current = loaded
+ return loaded
+ })
+ const loggedIn = useConfigState(s => s.loggedIn)
+ return loggedInLoaded && !loggedIn
+ }
+
+ const desktopModalScreensConfig = routeMapToStaticScreens(modalRoutes, makeLayout, true, false, false)
+
+ const desktopRootNav = createNativeStackNavigator({
+ groups: {
+ loggedIn: {
+ if: useIsLoggedInDesktop,
+ screens: {
+ loggedIn: {screen: AppTabsDesktop},
+ ...desktopModalScreensConfig,
+ },
+ },
+ loggedOut: {
+ if: useIsLoggedOutDesktop,
+ screens: {
+ loggedOut: {screen: LoggedOutDesktop},
+ },
+ },
+ },
+ screenOptions: desktopRootScreenOptions,
+ screens: {
+ loading: {
+ if: useIsLoadingDesktop,
+ screen: Shared.SimpleLoading,
+ },
+ },
+ })
+ DesktopRootComponent = desktopRootNav.getComponent()
+}
+
+const useConnectNavToState = () => {
+ const setNavOnce = React.useRef(false)
+ React.useEffect(() => {
+ if (!setNavOnce.current) {
+ if (C.Router2.navigationRef.isReady()) {
+ setNavOnce.current = true
+
+ if (__DEV__) {
+ const w = window as unknown as Record | undefined
+ if (w) {
+ w['DEBUGNavigator'] = C.Router2.navigationRef.current
+ w['DEBUGRouter2'] = C.Router2
+ w['KBCONSTANTS'] = require('@/constants')
+ w['KBINBOX'] = require('@/constants/chat')
+ const {registerDebugClear} = require('@/util/debug') as {registerDebugClear: (cb: () => void) => void}
+ registerDebugClear(() => {
+ w['DEBUGNavigator'] = undefined
+ w['DEBUGRouter2'] = undefined
+ w['KBCONSTANTS'] = undefined
+ w['KBINBOX'] = undefined
+ })
+ }
+ }
+ }
+ }
+ }, [setNavOnce])
+}
+
+function DesktopRouter() {
+ useConnectNavToState()
+
+ const onUnhandledAction = (a: Readonly<{type: string}>) => {
+ logger.info(`[NAV] Unhandled action: ${a.type}`, a, C.Router2.logState())
+ }
+
+ const setNavState = C.useRouterState(s => s.dispatch.setNavState)
+ const onStateChange = () => {
+ const ns = C.Router2.getRootState()
+ setNavState(ns)
+ }
+
+ const navRef = (ref: typeof C.Router2.navigationRef.current) => {
+ if (ref) {
+ C.Router2.navigationRef.current = ref
+ }
+ }
+
+ const isDarkMode = useDarkModeState(s => s.isDarkMode())
+ const username = useCurrentUserState(s => s.username)
+ // Only remount the navigator when switching between logged-in users.
+ // Ignore '' → username (initial login) so in-flight unbox requests aren't interrupted.
+ const [navKey, setNavKey] = React.useState('')
+ const prevUsernameRef = React.useRef(username)
+ React.useEffect(() => {
+ const prev = prevUsernameRef.current
+ prevUsernameRef.current = username
+ if (prev && username && prev !== username) {
+ setNavKey(username)
+ }
+ }, [username])
+
+ const documentTitle = {
+ formatter: () => {
+ const t = C.Router2.getTab()
+ const m = t ? C.Tabs.desktopTabMeta[t] : undefined
+ const tabLabel: string = m?.label ?? ''
+ return `Keybase: ${tabLabel}`
+ },
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+// ─── Native ───────────────────────────────────────────────────────────────────
+
+if (isMobile) {
+ if (module.hot) {
+ module.hot.accept('', () => {})
+ }
+}
+
+const tabToLabel = new Map([
+ [Tabs.chatTab, 'Chat'],
+ [Tabs.fsTab, 'Files'],
+ [Tabs.teamsTab, 'Teams'],
+ [Tabs.peopleTab, 'People'],
+ [Tabs.settingsTab, 'More'],
+])
+
+// just to get badge rollups
+const nativeTabs = C.isTablet ? Tabs.tabletTabs : Tabs.phoneTabs
+const settingsTabChildren = [Tabs.gitTab, Tabs.devicesTab, Tabs.settingsTab] as const
+
+const tabStackOptions = ({
+ navigation,
+}: {
+ navigation: {canGoBack: () => boolean}
+}): NativeStackNavigationOptions => ({
+ ...Common.defaultNavigationOptions,
+ // Use the native back button (liquid glass pill on iOS 26) for non-root screens;
+ // omit headerLeft entirely on root screens so no empty glass circle appears.
+ headerBackVisible: navigation.canGoBack(),
+ headerLeft: undefined,
+})
+
+// On phones, each tab stack only contains its root screen. All other routes live in
+// the root stack (alongside chatConversation) so they render above the tab bar.
+const tabRootNameSet = new Set(Object.values(tabRoots).filter(Boolean))
+const phoneRootRoutes = Object.fromEntries(
+ Object.entries(routes).filter(([name]) => !tabRootNameSet.has(name))
+) as typeof routes
+
+const nativeTabComponents: Record = {}
+
+if (isMobile) {
+ const nativeTabScreensConfig = routeMapToStaticScreens(routes, makeLayout, false, false, true)
+
+ for (const tab of nativeTabs) {
+ if (C.isTablet) {
+ const nav = createNativeStackNavigator({
+ initialRouteName: tabRoots[tab],
+ screenOptions: tabStackOptions,
+ screens: nativeTabScreensConfig,
+ })
+ nativeTabComponents[tab] = nav.getComponent()
+ } else {
+ const rootName = tabRoots[tab]
+ const rootScreenConfig = routeMapToStaticScreens(
+ {[rootName]: routes[rootName as keyof typeof routes]} as typeof routes,
+ makeLayout,
+ false,
+ false,
+ true
+ )
+ const nav = createNativeStackNavigator({
+ initialRouteName: rootName,
+ screenOptions: tabStackOptions,
+ screens: rootScreenConfig,
+ })
+ nativeTabComponents[tab] = nav.getComponent()
+ }
+ }
+}
+
+const androidTabIcons = new Map(
+ isMobile
+ ? [
+ [Tabs.chatTab, require('../images/icons/icon-nav-chat-32.png')],
+ [Tabs.fsTab, require('../images/icons/icon-nav-folders-32.png')],
+ [Tabs.peopleTab, require('../images/icons/icon-nav-people-32.png')],
+ [Tabs.settingsTab, require('../images/icons/icon-nav-settings-32.png')],
+ [Tabs.teamsTab, require('../images/icons/icon-nav-teams-32.png')],
+ ]
+ : []
+)
+
+const iosTabIcons = new Map(
+ isMobile
+ ? [
+ [Tabs.chatTab, {active: 'bubble.fill', inactive: 'bubble'}],
+ [Tabs.fsTab, {active: 'folder.fill', inactive: 'folder'}],
+ [Tabs.peopleTab, {active: 'person.crop.rectangle.fill', inactive: 'person.crop.rectangle'}],
+ [Tabs.settingsTab, {active: 'line.3.horizontal.circle.fill', inactive: 'line.3.horizontal'}],
+ [Tabs.teamsTab, {active: 'person.2.fill', inactive: 'person.2'}],
+ ]
+ : []
+)
+
+const getNativeTabIcon = (tab: Tabs.Tab) => {
+ if (isIOS) {
+ const icon = iosTabIcons.get(tab)
+ return icon
+ ? ({focused}: {focused: boolean}) => ({
+ name: focused ? icon.active : icon.inactive,
+ type: 'sfSymbol' as const,
+ })
+ : undefined
+ }
+ const source = androidTabIcons.get(tab)
+ return source ? {source, type: 'image' as const} : undefined
+}
+
+const getBadgeNumber = (
+ routeName: Tabs.Tab,
+ navBadges: ReadonlyMap,
+ hasPermissions: boolean
+) => {
+ const onSettings = routeName === Tabs.settingsTab
+ const tabsToCount: ReadonlyArray = onSettings ? settingsTabChildren : [routeName]
+ const count = tabsToCount.reduce(
+ (res, tab) => res + (navBadges.get(tab) || 0),
+ onSettings && !hasPermissions ? 1 : 0
+ )
+ return count || undefined
+}
+
+const appTabsScreenOptions = (
+ routeName: Tabs.Tab,
+ navBadges: ReadonlyMap