mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(relay): install the desktop Relay provider on demand (#27159)
* fix(relay): install the desktop Relay provider on demand * fix(relay): suspend Relay installs across sign-out and bound the mint's install wait --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { DesktopRelayService } from './desktop-relay-service'
|
||||
import { createDesktopRelayServiceInstaller } from './desktop-relay-service-installer'
|
||||
|
||||
function fakeService(): DesktopRelayService {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the installer only calls start() and wraps the provider methods.
|
||||
return { start: vi.fn() } as unknown as DesktopRelayService
|
||||
}
|
||||
|
||||
function installerWith(overrides: {
|
||||
create: () => DesktopRelayService | null
|
||||
isFenced?: () => boolean
|
||||
whenNetworkReady?: Promise<unknown>
|
||||
}) {
|
||||
const runtimeRpc = { setMobileRelayPairingProvider: vi.fn() }
|
||||
const onInstalled = vi.fn()
|
||||
const installer = createDesktopRelayServiceInstaller({
|
||||
runtimeRpc,
|
||||
whenNetworkReady: overrides.whenNetworkReady ?? Promise.resolve(),
|
||||
isFenced: overrides.isFenced ?? (() => false),
|
||||
create: overrides.create,
|
||||
onInstalled
|
||||
})
|
||||
return { installer, runtimeRpc, onInstalled }
|
||||
}
|
||||
|
||||
describe('createDesktopRelayServiceInstaller', () => {
|
||||
it('retries a construction that failed at launch on the next demand, without a restart', async () => {
|
||||
const service = fakeService()
|
||||
const create = vi
|
||||
.fn<() => DesktopRelayService | null>()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error('mobile_runtime_not_ready')
|
||||
})
|
||||
.mockReturnValueOnce(service)
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { installer, runtimeRpc, onInstalled } = installerWith({ create })
|
||||
|
||||
await expect(installer.ensure()).resolves.toBe(false)
|
||||
expect(runtimeRpc.setMobileRelayPairingProvider).not.toHaveBeenCalled()
|
||||
await expect(installer.ensure()).resolves.toBe(true)
|
||||
expect(onInstalled).toHaveBeenCalledWith(service)
|
||||
expect(runtimeRpc.setMobileRelayPairingProvider).toHaveBeenCalledOnce()
|
||||
expect(service.start).toHaveBeenCalledOnce()
|
||||
await expect(installer.ensure()).resolves.toBe(true)
|
||||
expect(create).toHaveBeenCalledTimes(2)
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('holds the first install until the network is ready and shares one in-flight install', async () => {
|
||||
let ready!: () => void
|
||||
const whenNetworkReady = new Promise<void>((resolve) => (ready = resolve))
|
||||
const create = vi.fn(() => fakeService())
|
||||
const { installer } = installerWith({ create, whenNetworkReady })
|
||||
|
||||
const first = installer.ensure()
|
||||
const second = installer.ensure()
|
||||
await Promise.resolve()
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
ready()
|
||||
await expect(Promise.all([first, second])).resolves.toEqual([true, true])
|
||||
expect(create).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('refuses installs after sign-out, including one already waiting, until the next auth change', async () => {
|
||||
let ready!: () => void
|
||||
const whenNetworkReady = new Promise<void>((resolve) => (ready = resolve))
|
||||
const create = vi.fn(() => fakeService())
|
||||
const { installer } = installerWith({ create, whenNetworkReady })
|
||||
|
||||
const waiting = installer.ensure()
|
||||
installer.suspend()
|
||||
ready()
|
||||
await expect(waiting).resolves.toBe(false)
|
||||
await expect(installer.ensure()).resolves.toBe(false)
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
await expect(installer.authChanged()).resolves.toBe(true)
|
||||
expect(create).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('never installs once quit or relaunch fenced the host', async () => {
|
||||
const create = vi.fn(() => fakeService())
|
||||
const { installer } = installerWith({ create, isFenced: () => true })
|
||||
|
||||
await expect(installer.ensure()).resolves.toBe(false)
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { MobileRelayPairingProvider } from '../runtime-rpc/runtime-rpc-pairing-types'
|
||||
import type { DesktopRelayService } from './desktop-relay-service'
|
||||
|
||||
type RelayProviderHost = {
|
||||
setMobileRelayPairingProvider(provider: MobileRelayPairingProvider | null): void
|
||||
}
|
||||
|
||||
export type DesktopRelayServiceInstallerOptions = {
|
||||
runtimeRpc: RelayProviderHost
|
||||
// App-owned fetchers must not run ahead of the persisted proxy.
|
||||
whenNetworkReady: Promise<unknown>
|
||||
// Quit/relaunch fencing: a fenced host never installs a new service.
|
||||
isFenced: () => boolean
|
||||
// Returns null when this build has no cloud auth; throws on a failed construction.
|
||||
create: () => DesktopRelayService | null
|
||||
onInstalled: (service: DesktopRelayService) => void
|
||||
}
|
||||
|
||||
export type DesktopRelayServiceInstaller = {
|
||||
ensure(): Promise<boolean>
|
||||
// Sign-out: refuse installs, including one already waiting, until the next auth change.
|
||||
suspend(): void
|
||||
authChanged(): Promise<boolean>
|
||||
}
|
||||
|
||||
function pairingProviderFor(service: DesktopRelayService): MobileRelayPairingProvider {
|
||||
return {
|
||||
createPairingRelay: (relayDeviceId) => service.createPairingRelay(relayDeviceId),
|
||||
onDeviceRevokeQueued: (item) => service.onDeviceRevokeQueued(item),
|
||||
onDemandStateChanged: () => service.demandStateChanged(),
|
||||
getEndpoints: (context, params) => service.getEndpoints(context, params),
|
||||
provisionRelay: (context, params) => service.provisionRelay(context, params)
|
||||
}
|
||||
}
|
||||
|
||||
// Installs the Relay provider once, on demand: at launch, after a cloud auth
|
||||
// change, and before an automatic pairing mint. A construction that failed once
|
||||
// (or ran before the proxy landed) is retried by the next demand, not a restart.
|
||||
export function createDesktopRelayServiceInstaller(
|
||||
options: DesktopRelayServiceInstallerOptions
|
||||
): DesktopRelayServiceInstaller {
|
||||
let installed: DesktopRelayService | null = null
|
||||
let pending: Promise<boolean> | null = null
|
||||
let suspended = false
|
||||
const install = async (): Promise<boolean> => {
|
||||
await options.whenNetworkReady
|
||||
if (installed || suspended || options.isFenced()) {
|
||||
return installed !== null
|
||||
}
|
||||
try {
|
||||
const service = options.create()
|
||||
if (!service) {
|
||||
return false
|
||||
}
|
||||
installed = service
|
||||
options.onInstalled(service)
|
||||
options.runtimeRpc.setMobileRelayPairingProvider(pairingProviderFor(service))
|
||||
service.start()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[relay] Desktop relay startup unavailable:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
const ensure = (): Promise<boolean> => {
|
||||
if (installed) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
pending ??= install().finally(() => {
|
||||
pending = null
|
||||
})
|
||||
return pending
|
||||
}
|
||||
return {
|
||||
ensure,
|
||||
suspend: () => {
|
||||
suspended = true
|
||||
},
|
||||
authChanged: () => {
|
||||
suspended = false
|
||||
return ensure()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { OrcaRuntimeRpcServer } from './runtime-rpc'
|
||||
|
||||
vi.mock('../git/worktree', () => ({
|
||||
listWorktrees: vi.fn().mockResolvedValue([]),
|
||||
listWorktreesStrict: vi.fn().mockResolvedValue([])
|
||||
}))
|
||||
|
||||
describe('automatic mobile pairing with a missing Relay provider (#20005)', () => {
|
||||
it('installs the provider on demand instead of failing provider_missing', async () => {
|
||||
const server = new OrcaRuntimeRpcServer({
|
||||
runtime: new OrcaRuntimeService(),
|
||||
userDataPath: mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')),
|
||||
enableWebSocket: true,
|
||||
wsPort: 0
|
||||
})
|
||||
const relay = {
|
||||
v: 1 as const,
|
||||
directorUrl: 'https://relay.example.com',
|
||||
cellUrl: 'https://cell.example.com',
|
||||
assignmentEpoch: 7,
|
||||
relayHostId: 'AbCdEf0123_-xyZ9',
|
||||
inviteToken: 'A'.repeat(43),
|
||||
inviteExpiresAt: Date.now() + 60_000,
|
||||
e2eeFraming: 2 as const
|
||||
}
|
||||
const install = vi.fn(async () => {
|
||||
server.setMobileRelayPairingProvider({
|
||||
createPairingRelay: async (relayDeviceId) => ({
|
||||
relay,
|
||||
binding: { relayHostId: relay.relayHostId, relayDeviceId, ownerIdentityKey: 'owner' }
|
||||
}),
|
||||
onDeviceRevokeQueued: vi.fn(),
|
||||
getEndpoints: vi.fn(),
|
||||
provisionRelay: vi.fn()
|
||||
})
|
||||
})
|
||||
server.setMobileRelayPairingProviderInstaller(install)
|
||||
|
||||
await server.start()
|
||||
try {
|
||||
const offer = await server.createMobilePairingOffer({ address: '100.64.1.20' })
|
||||
expect(install).toHaveBeenCalledOnce()
|
||||
expect(offer).toMatchObject({ available: true, connectionMode: 'automatic' })
|
||||
} finally {
|
||||
await server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('reports the provider missing when an install outlives the mint budget', async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||
const server = new OrcaRuntimeRpcServer({
|
||||
runtime: new OrcaRuntimeService(),
|
||||
userDataPath: mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')),
|
||||
enableWebSocket: true,
|
||||
wsPort: 0
|
||||
})
|
||||
server.setMobileRelayPairingProviderInstaller(() => new Promise(() => {}))
|
||||
await server.start()
|
||||
try {
|
||||
const offer = server.createMobilePairingOffer({ address: '100.64.1.20' })
|
||||
await vi.advanceTimersByTimeAsync(15_000)
|
||||
await expect(offer).resolves.toMatchObject({
|
||||
available: false,
|
||||
relayFailure: { stage: 'provider_missing' }
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
await server.stop()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../../shared/pairing'
|
||||
import { NETWORK_EXPOSURE_FAILED_GUIDANCE } from '../network-exposure-guidance'
|
||||
import { RuntimeRpcPairing } from './runtime-rpc-pairing'
|
||||
import { withTimeout } from '../../../shared/promise-timeout-fallback'
|
||||
import {
|
||||
DEVICE_REGISTRY_UNAVAILABLE_GUIDANCE,
|
||||
pairingUnavailable,
|
||||
@@ -14,6 +15,9 @@ import {
|
||||
type PairingOfferUnavailable
|
||||
} from './runtime-rpc-pairing-types'
|
||||
|
||||
// How long an automatic mint waits for an on-demand Relay install before reporting it missing.
|
||||
const RELAY_PROVIDER_INSTALL_WAIT_MS = 15_000
|
||||
|
||||
export class RuntimeRpcMobilePairing extends RuntimeRpcPairing {
|
||||
async createMobilePairingOffer(args: {
|
||||
address?: string | null
|
||||
@@ -145,6 +149,14 @@ export class RuntimeRpcMobilePairing extends RuntimeRpcPairing {
|
||||
relayFailure
|
||||
}
|
||||
}
|
||||
if (!this.mobileRelayPairingProvider && this.mobileRelayPairingProviderInstaller) {
|
||||
// Bounded: the install waits for the persisted proxy, which has no time limit of its own.
|
||||
await withTimeout(
|
||||
this.mobileRelayPairingProviderInstaller(),
|
||||
RELAY_PROVIDER_INSTALL_WAIT_MS,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
const relayProvider = this.mobileRelayPairingProvider
|
||||
if (!relayProvider) {
|
||||
return refuseAutomaticWithoutRelay({
|
||||
|
||||
@@ -86,6 +86,10 @@ export class RuntimeRpcPairing extends RuntimeRpcNetworkExposure {
|
||||
this.mobileRelayPairingProvider = provider
|
||||
}
|
||||
|
||||
setMobileRelayPairingProviderInstaller(install: (() => Promise<unknown>) | null): void {
|
||||
this.mobileRelayPairingProviderInstaller = install
|
||||
}
|
||||
|
||||
async revokeMobileDevice(deviceId: string): Promise<boolean> {
|
||||
const device = this.deviceRegistry?.getDevice(deviceId)
|
||||
if (device?.scope !== 'mobile') {
|
||||
|
||||
@@ -70,6 +70,8 @@ export class RuntimeRpcState {
|
||||
// transports under the SAME wiring (see ensureMobileSocketWiring) instead of orphaning relay sockets.
|
||||
protected detachWebSocketWiring: (() => void) | null = null
|
||||
protected mobileRelayPairingProvider: MobileRelayPairingProvider | null = null
|
||||
// Why: lets an automatic mint install a provider whose launch-time construction failed.
|
||||
protected mobileRelayPairingProviderInstaller: (() => Promise<unknown>) | null = null
|
||||
protected mobileRelayPairingOfferQueue: Promise<void> = Promise.resolve()
|
||||
protected mobileRelayPairingOfferInFlight: {
|
||||
generation: number
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { app, powerMonitor } from 'electron'
|
||||
import { getOrcaCloudAuthConfig } from '../orca-profiles/profile-cloud-auth-config'
|
||||
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
|
||||
import type { OrcaRuntimeRpcServer } from '../runtime/runtime-rpc'
|
||||
import { DesktopRelayService } from '../runtime/relay/desktop-relay-service'
|
||||
import { createDesktopRelayServiceInstaller } from '../runtime/relay/desktop-relay-service-installer'
|
||||
import { publishDesktopRelayStatus } from './main-process-relay-status'
|
||||
import { mainProcessState as state } from './main-process-state'
|
||||
|
||||
// Registers the on-demand Relay installer and starts the first install, which
|
||||
// waits for the persisted proxy itself, so it can run before the proxy lands.
|
||||
export function startDesktopRelayService(runtimeRpc: OrcaRuntimeRpcServer): void {
|
||||
const installer = createDesktopRelayServiceInstaller({
|
||||
runtimeRpc,
|
||||
whenNetworkReady: state.initialProxyApplicationReady,
|
||||
isFenced: () => state.isQuitting,
|
||||
create: () => {
|
||||
const cloudAuth = getOrcaCloudAuthConfig()
|
||||
return cloudAuth.configured
|
||||
? new DesktopRelayService({
|
||||
authConfig: cloudAuth.config,
|
||||
userDataPath: getProfileUserDataPath(),
|
||||
appVersion: app.getVersion(),
|
||||
runtimeRpc,
|
||||
onStatus: publishDesktopRelayStatus
|
||||
})
|
||||
: null
|
||||
},
|
||||
onInstalled: (service) => {
|
||||
state.desktopRelayService = service
|
||||
}
|
||||
})
|
||||
state.desktopRelayInstaller = installer
|
||||
runtimeRpc.setMobileRelayPairingProviderInstaller(() => installer.ensure())
|
||||
// Why: sleeping past relay-token expiry kills the broker with no retry
|
||||
// timer; resume is the moment that state becomes recoverable.
|
||||
powerMonitor.on('resume', () => state.desktopRelayService?.ensureLive())
|
||||
void installer.ensure()
|
||||
}
|
||||
@@ -50,7 +50,7 @@ vi.mock('./main-process-relay-status', () => ({
|
||||
getDesktopRelayStatus: vi.fn(),
|
||||
publishDesktopRelayStatus: vi.fn()
|
||||
}))
|
||||
vi.mock('../runtime/relay/desktop-relay-service', () => ({ DesktopRelayService: class {} }))
|
||||
vi.mock('./main-process-relay-startup', () => ({ startDesktopRelayService: vi.fn() }))
|
||||
vi.mock('./main-process-serve', () => ({
|
||||
getServeOptions: vi.fn(() => null),
|
||||
getBundledWebClientRoot: vi.fn(() => null),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { app, powerMonitor, type BrowserWindow } from 'electron'
|
||||
import { app, type BrowserWindow } from 'electron'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { getOrcaCloudAuthConfig } from '../orca-profiles/profile-cloud-auth-config'
|
||||
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
|
||||
import {
|
||||
getCanonicalUserDataPath,
|
||||
migrateMobilePairingDataToCanonicalUserDataPath
|
||||
@@ -13,8 +11,8 @@ import { LocalPtyProvider } from '../providers/local-pty-provider'
|
||||
import { publishHeadlessRuntimeGraph } from '../runtime/headless-runtime-graph'
|
||||
import { OffscreenBrowserBackend } from '../browser/offscreen-browser-backend'
|
||||
import { browserManager } from '../browser/browser-manager'
|
||||
import { getDesktopRelayStatus, publishDesktopRelayStatus } from './main-process-relay-status'
|
||||
import { DesktopRelayService } from '../runtime/relay/desktop-relay-service'
|
||||
import { getDesktopRelayStatus } from './main-process-relay-status'
|
||||
import { startDesktopRelayService } from './main-process-relay-startup'
|
||||
import { getServeOptions, getBundledWebClientRoot, printServeReady } from './main-process-serve'
|
||||
import {
|
||||
bindTerminalRuntimeStartupServices,
|
||||
@@ -249,42 +247,16 @@ async function launchDesktopMode(
|
||||
showRuntimeRpcStartupFailureDialog(win, runtimeRpcStartResult.error)
|
||||
)
|
||||
}
|
||||
// Why before the proxy await: a mint in that window must wait for the installer, not fail
|
||||
// provider_missing; the installer itself holds its first request until the proxy lands.
|
||||
startDesktopRelayService(runtimeRpc)
|
||||
// Why after the window and not before it: the default-session request guard already holds every
|
||||
// fetcher until the persisted proxy lands, so this only has to keep the launch phase itself
|
||||
// ordered ahead of the relay — it must not gate the renderer.
|
||||
// ordered ahead of the push gateway — it must not gate the renderer.
|
||||
await state.initialProxyApplicationReady
|
||||
// Why after the proxy await: the push gateway client is an app-owned fetcher, so it must not
|
||||
// issue its first request ahead of the persisted proxy.
|
||||
startDesktopPushService(runtimeRpc)
|
||||
const cloudAuth = getOrcaCloudAuthConfig()
|
||||
if (cloudAuth.configured) {
|
||||
try {
|
||||
const relayService = new DesktopRelayService({
|
||||
authConfig: cloudAuth.config,
|
||||
userDataPath: getProfileUserDataPath(),
|
||||
appVersion: app.getVersion(),
|
||||
runtimeRpc,
|
||||
onStatus: publishDesktopRelayStatus
|
||||
})
|
||||
state.desktopRelayService = relayService
|
||||
runtimeRpc.setMobileRelayPairingProvider({
|
||||
createPairingRelay: (relayDeviceId) => relayService.createPairingRelay(relayDeviceId),
|
||||
onDeviceRevokeQueued: (item) => relayService.onDeviceRevokeQueued(item),
|
||||
onDemandStateChanged: () => relayService.demandStateChanged(),
|
||||
getEndpoints: (context, params) => relayService.getEndpoints(context, params),
|
||||
provisionRelay: (context, params) => relayService.provisionRelay(context, params)
|
||||
})
|
||||
relayService.start()
|
||||
// Why: sleeping past relay-token expiry kills the broker with no retry
|
||||
// timer; resume is the moment that state becomes recoverable.
|
||||
powerMonitor.on('resume', () => state.desktopRelayService?.ensureLive())
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[relay] Desktop relay startup unavailable:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Why: macOS notification permission dialog must fire after the window is shown, else it's hidden behind the maximized window.
|
||||
win.once('show', () => {
|
||||
// Why: store can be null if init failed earlier; bail rather than throw inside an Electron event listener.
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
||||
import type { RateLimitService } from '../rate-limits/service'
|
||||
import type { OrcaRuntimeRpcServer } from '../runtime/runtime-rpc'
|
||||
import type { DesktopRelayService } from '../runtime/relay/desktop-relay-service'
|
||||
import type { DesktopRelayServiceInstaller } from '../runtime/relay/desktop-relay-service-installer'
|
||||
import type { DesktopPushService } from '../runtime/push/desktop-push-service'
|
||||
import type { StarNagService } from '../star-nag/service'
|
||||
import type { AgentAwakeService } from '../agent-awake-service'
|
||||
@@ -87,6 +88,8 @@ export const mainProcessState = {
|
||||
runtimeRpc: null as OrcaRuntimeRpcServer | null,
|
||||
serveReadinessPublisher: new ServeReadinessPublisher(),
|
||||
desktopRelayService: null as DesktopRelayService | null,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: widens the null slot to the installer assigned by main-process-relay-startup.
|
||||
desktopRelayInstaller: null as DesktopRelayServiceInstaller | null,
|
||||
desktopPushService: null as DesktopPushService | null,
|
||||
desktopRelayStatus: 'offline' as RelayBrokerStatus,
|
||||
desktopRelayCellUrl: undefined as string | undefined,
|
||||
|
||||
@@ -95,11 +95,17 @@ export function attachMainWindowCoreServices(
|
||||
state.desktopRelayService?.fenceAndCloseNow()
|
||||
await preserveAgentAuthBeforeRestart({ codexRuntimeHome, store })
|
||||
},
|
||||
onOrcaProfileAuthMutation: () => state.desktopRelayService?.authMutated(),
|
||||
onOrcaProfileAuthMutation: () => {
|
||||
// Why: a provider whose launch-time install failed is installed by the next auth change.
|
||||
void state.desktopRelayInstaller?.authChanged()
|
||||
state.desktopRelayService?.authMutated()
|
||||
},
|
||||
// Sign-out is the one fence a paired phone can be told about; quit and
|
||||
// relaunch above stay reasonless so a restart never reads as signed out.
|
||||
onBeforeOrcaProfileSignOut: () =>
|
||||
onBeforeOrcaProfileSignOut: () => {
|
||||
state.desktopRelayInstaller?.suspend()
|
||||
state.desktopRelayService?.fenceAndCloseNow(RELAY_HOST_CLOSE_REASON.SIGNED_OUT)
|
||||
}
|
||||
},
|
||||
state.pluginService ?? undefined,
|
||||
state.pluginMarketplaceService && state.pluginMarketplaceInstaller
|
||||
|
||||
@@ -220,7 +220,7 @@ describe('HeroFlow height', () => {
|
||||
await waitFor(() => expect(screen.getByText(/Creating a new pairing code/)).toBeVisible())
|
||||
})
|
||||
|
||||
it('does not offer a futile retry when Relay is unavailable on the desktop', () => {
|
||||
it('offers Retry Relay when the desktop provider is missing, since a retry installs it', () => {
|
||||
renderFlow(1, {
|
||||
relayMintFailure: {
|
||||
code: 'relay_provider_unavailable',
|
||||
@@ -231,7 +231,7 @@ describe('HeroFlow height', () => {
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(
|
||||
'Orca Relay isn’t available on this desktop'
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: 'Retry Relay' })).toBeNull()
|
||||
expect(screen.getByRole('button', { name: 'Retry Relay' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'Use LAN' })).toBeEnabled()
|
||||
})
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ export function MobileRelayMintFailureNotice({
|
||||
<Button type="button" size={compact ? 'xs' : 'sm'} onClick={onUseLan}>
|
||||
{translate('auto.components.mobile.MobileRelayMintFailureNotice.useLan', 'Use LAN')}
|
||||
</Button>
|
||||
{!providerMissing && !reconnectRequired && !notEntitled ? (
|
||||
{!reconnectRequired && !notEntitled ? (
|
||||
<Button
|
||||
type="button"
|
||||
size={compact ? 'xs' : 'sm'}
|
||||
|
||||
Reference in New Issue
Block a user