From bb35b283da2149d09a04f5c40dc96e3c0be4c213 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 9 Jun 2026 01:33:24 +0000
Subject: [PATCH 1/2] refactor: consolidate WebLoginStartResult and
WebLoginWaitResult into WebLoginResult
WebLoginStartResult and WebLoginWaitResult were byte-for-byte identical:
both had the same 5 properties (Message, QrDataUrl, Connected, Error,
RawResponse). The method names (WebLoginStartAsync / WebLoginWaitAsync)
already distinguish the two flows semantically, so maintaining two
separate types added no value.
Replace both with a single WebLoginResult class. Update IOperatorGatewayClient,
OpenClawGatewayClient, and the FakeOperatorGatewayClient test stub accordingly.
ChannelsPage.xaml.cs uses both via property access with inferred types and
required no changes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/OpenClaw.Shared/ChannelsSnapshot.cs | 22 +++++--------------
src/OpenClaw.Shared/IOperatorGatewayClient.cs | 4 ++--
src/OpenClaw.Shared/OpenClawGatewayClient.cs | 12 +++++-----
.../OnboardingChatBootstrapperTests.cs | 4 ++--
4 files changed, 16 insertions(+), 26 deletions(-)
diff --git a/src/OpenClaw.Shared/ChannelsSnapshot.cs b/src/OpenClaw.Shared/ChannelsSnapshot.cs
index 721aaa3c5..b5866e1f7 100644
--- a/src/OpenClaw.Shared/ChannelsSnapshot.cs
+++ b/src/OpenClaw.Shared/ChannelsSnapshot.cs
@@ -234,22 +234,12 @@ public sealed class IMessageChannelStatus
// ─── Web login (QR linking) ──────────────────────────────────────────────────
-/// Result of web.login.start — a QR or status for a channel.
-public sealed class WebLoginStartResult
-{
- public string? Message { get; init; }
- public string? QrDataUrl { get; init; }
- public bool Connected { get; init; }
-
- /// Gateway-side error message when the call failed (ok=false), or transport exception. Null on success.
- public string? Error { get; init; }
-
- /// Raw JSON of the gateway response (or stringified exception). Used by the diagnostic disclosure in the UI.
- public string? RawResponse { get; init; }
-}
-
-/// Result of web.login.wait — long-poll outcome.
-public sealed class WebLoginWaitResult
+///
+/// Result of a QR/web-login RPC call (web.login.start or web.login.wait).
+/// Both calls return an identical wire shape; a single type removes duplication while
+/// the calling method name already distinguishes the two flows.
+///
+public sealed class WebLoginResult
{
public string? Message { get; init; }
public string? QrDataUrl { get; init; }
diff --git a/src/OpenClaw.Shared/IOperatorGatewayClient.cs b/src/OpenClaw.Shared/IOperatorGatewayClient.cs
index 2d59f0389..d5ef81e54 100644
--- a/src/OpenClaw.Shared/IOperatorGatewayClient.cs
+++ b/src/OpenClaw.Shared/IOperatorGatewayClient.cs
@@ -107,8 +107,8 @@ public interface IOperatorGatewayClient
/// Log out / unlink a channel (whatsapp, telegram). Sends channels.logout { channel }.
Task LogoutChannelAsync(string channelName, int timeoutMs = 12000);
/// Begin a QR linking flow (whatsapp, signal). Sends web.login.start { force, timeoutMs }.
- Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000);
+ Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000);
/// Long-poll for QR linking completion. Sends web.login.wait { currentQrDataUrl, timeoutMs }.
- Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000);
+ Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000);
Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000);
}
diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.cs
index 2903700f4..ba1cbff1b 100644
--- a/src/OpenClaw.Shared/OpenClawGatewayClient.cs
+++ b/src/OpenClaw.Shared/OpenClawGatewayClient.cs
@@ -1210,7 +1210,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
}
/// Begin a web/QR linking flow for the current default linking channel.
- public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000)
+ public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000)
{
if (!IsConnected) return null;
try
@@ -1219,7 +1219,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
"web.login.start",
new { force, timeoutMs },
timeoutMs + 5000);
- return new WebLoginStartResult
+ return new WebLoginResult
{
Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null,
QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null,
@@ -1233,7 +1233,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
// Return a populated result with the error so the UI can surface
// it in the diagnostic disclosure. Returning null would lose the
// gateway's actual reason for failing.
- return new WebLoginStartResult
+ return new WebLoginResult
{
Error = ex.Message,
RawResponse = ex.ToString(),
@@ -1242,7 +1242,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
}
/// Long-poll for QR linking completion.
- public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000)
+ public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000)
{
if (!IsConnected) return null;
try
@@ -1251,7 +1251,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
"web.login.wait",
new { currentQrDataUrl, timeoutMs },
timeoutMs + 5000);
- return new WebLoginWaitResult
+ return new WebLoginResult
{
Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null,
QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null,
@@ -1262,7 +1262,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
catch (Exception ex)
{
_logger.Warn($"web.login.wait failed: {ex.Message}");
- return new WebLoginWaitResult
+ return new WebLoginResult
{
Error = ex.Message,
RawResponse = ex.ToString(),
diff --git a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs
index d6843c572..adcb9b9d7 100644
--- a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs
+++ b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs
@@ -200,8 +200,8 @@ public Task NodeRenameAsync(string nodeId, string displayName)
public Task StopChannelAsync(string channelName) => Task.FromResult(false);
public Task GetChannelsStatusAsync(bool probe = false, int timeoutMs = 12000) => Task.FromResult(null);
public Task LogoutChannelAsync(string channelName, int timeoutMs = 12000) => Task.FromResult(false);
- public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null);
- public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null);
+ public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null);
+ public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null);
public Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000) => Task.FromResult(default(JsonElement));
}
#pragma warning restore CS0067
From 7601ceaab11072a502b18516ec7c0c595f8fa836 Mon Sep 17 00:00:00 2001
From: ranjeshj
Date: Wed, 17 Jun 2026 23:41:27 -0700
Subject: [PATCH 2/2] Preserve web login result API
Restore the public WebLoginStartResult and WebLoginWaitResult contract while keeping shared response parsing inside OpenClawGatewayClient.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/OpenClaw.Shared/ChannelsSnapshot.cs | 22 +++++++---
src/OpenClaw.Shared/IOperatorGatewayClient.cs | 4 +-
src/OpenClaw.Shared/OpenClawGatewayClient.cs | 41 ++++++++++++-------
.../OnboardingChatBootstrapperTests.cs | 4 +-
4 files changed, 47 insertions(+), 24 deletions(-)
diff --git a/src/OpenClaw.Shared/ChannelsSnapshot.cs b/src/OpenClaw.Shared/ChannelsSnapshot.cs
index b5866e1f7..a598f69ad 100644
--- a/src/OpenClaw.Shared/ChannelsSnapshot.cs
+++ b/src/OpenClaw.Shared/ChannelsSnapshot.cs
@@ -234,12 +234,22 @@ public sealed class IMessageChannelStatus
// ─── Web login (QR linking) ──────────────────────────────────────────────────
-///
-/// Result of a QR/web-login RPC call (web.login.start or web.login.wait).
-/// Both calls return an identical wire shape; a single type removes duplication while
-/// the calling method name already distinguishes the two flows.
-///
-public sealed class WebLoginResult
+/// Result of web.login.start - a QR or status for a channel.
+public sealed class WebLoginStartResult
+{
+ public string? Message { get; init; }
+ public string? QrDataUrl { get; init; }
+ public bool Connected { get; init; }
+
+ /// Gateway-side error message when the call failed (ok=false), or transport exception. Null on success.
+ public string? Error { get; init; }
+
+ /// Raw JSON of the gateway response (or stringified exception). Used by the diagnostic disclosure in the UI.
+ public string? RawResponse { get; init; }
+}
+
+/// Result of web.login.wait - long-poll outcome.
+public sealed class WebLoginWaitResult
{
public string? Message { get; init; }
public string? QrDataUrl { get; init; }
diff --git a/src/OpenClaw.Shared/IOperatorGatewayClient.cs b/src/OpenClaw.Shared/IOperatorGatewayClient.cs
index d5ef81e54..2d59f0389 100644
--- a/src/OpenClaw.Shared/IOperatorGatewayClient.cs
+++ b/src/OpenClaw.Shared/IOperatorGatewayClient.cs
@@ -107,8 +107,8 @@ public interface IOperatorGatewayClient
/// Log out / unlink a channel (whatsapp, telegram). Sends channels.logout { channel }.
Task LogoutChannelAsync(string channelName, int timeoutMs = 12000);
/// Begin a QR linking flow (whatsapp, signal). Sends web.login.start { force, timeoutMs }.
- Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000);
+ Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000);
/// Long-poll for QR linking completion. Sends web.login.wait { currentQrDataUrl, timeoutMs }.
- Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000);
+ Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000);
Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000);
}
diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.cs
index ba1cbff1b..5a6c8cd21 100644
--- a/src/OpenClaw.Shared/OpenClawGatewayClient.cs
+++ b/src/OpenClaw.Shared/OpenClawGatewayClient.cs
@@ -1210,7 +1210,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
}
/// Begin a web/QR linking flow for the current default linking channel.
- public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000)
+ public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000)
{
if (!IsConnected) return null;
try
@@ -1219,12 +1219,13 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
"web.login.start",
new { force, timeoutMs },
timeoutMs + 5000);
- return new WebLoginResult
+ var result = ParseWebLoginResponse(response);
+ return new WebLoginStartResult
{
- Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null,
- QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null,
- Connected = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("connected", out var c) && c.ValueKind == JsonValueKind.True,
- RawResponse = response.ValueKind != JsonValueKind.Undefined ? response.GetRawText() : null,
+ Message = result.Message,
+ QrDataUrl = result.QrDataUrl,
+ Connected = result.Connected,
+ RawResponse = result.RawResponse,
};
}
catch (Exception ex)
@@ -1233,7 +1234,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
// Return a populated result with the error so the UI can surface
// it in the diagnostic disclosure. Returning null would lose the
// gateway's actual reason for failing.
- return new WebLoginResult
+ return new WebLoginStartResult
{
Error = ex.Message,
RawResponse = ex.ToString(),
@@ -1242,7 +1243,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
}
/// Long-poll for QR linking completion.
- public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000)
+ public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000)
{
if (!IsConnected) return null;
try
@@ -1251,18 +1252,19 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
"web.login.wait",
new { currentQrDataUrl, timeoutMs },
timeoutMs + 5000);
- return new WebLoginResult
+ var result = ParseWebLoginResponse(response);
+ return new WebLoginWaitResult
{
- Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null,
- QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null,
- Connected = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("connected", out var c) && c.ValueKind == JsonValueKind.True,
- RawResponse = response.ValueKind != JsonValueKind.Undefined ? response.GetRawText() : null,
+ Message = result.Message,
+ QrDataUrl = result.QrDataUrl,
+ Connected = result.Connected,
+ RawResponse = result.RawResponse,
};
}
catch (Exception ex)
{
_logger.Warn($"web.login.wait failed: {ex.Message}");
- return new WebLoginResult
+ return new WebLoginWaitResult
{
Error = ex.Message,
RawResponse = ex.ToString(),
@@ -1270,6 +1272,17 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1
}
}
+ private static WebLoginResponse ParseWebLoginResponse(JsonElement response)
+ {
+ return new WebLoginResponse(
+ response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null,
+ response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null,
+ response.ValueKind == JsonValueKind.Object && response.TryGetProperty("connected", out var c) && c.ValueKind == JsonValueKind.True,
+ response.ValueKind != JsonValueKind.Undefined ? response.GetRawText() : null);
+ }
+
+ private readonly record struct WebLoginResponse(string? Message, string? QrDataUrl, bool Connected, string? RawResponse);
+
private async Task SendConnectMessageAsync(string? nonce = null)
{
var requestId = Guid.NewGuid().ToString();
diff --git a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs
index adcb9b9d7..d6843c572 100644
--- a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs
+++ b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs
@@ -200,8 +200,8 @@ public Task NodeRenameAsync(string nodeId, string displayName)
public Task StopChannelAsync(string channelName) => Task.FromResult(false);
public Task GetChannelsStatusAsync(bool probe = false, int timeoutMs = 12000) => Task.FromResult(null);
public Task LogoutChannelAsync(string channelName, int timeoutMs = 12000) => Task.FromResult(false);
- public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null);
- public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null);
+ public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null);
+ public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null);
public Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000) => Task.FromResult(default(JsonElement));
}
#pragma warning restore CS0067