diff --git a/Mythic/Utilities/GameManager/Legendary/LegendaryInterface+Extensions.swift b/Mythic/Utilities/GameManager/Legendary/LegendaryInterface+Extensions.swift index a3ec91fe..3d61c6b2 100644 --- a/Mythic/Utilities/GameManager/Legendary/LegendaryInterface+Extensions.swift +++ b/Mythic/Utilities/GameManager/Legendary/LegendaryInterface+Extensions.swift @@ -266,8 +266,8 @@ extension Legendary { let clientID: String /// Client service name (e.g., "launcher") let clientService: String - /// Unique device identifier - let deviceID: String + /// Unique device identifier (no longer included in newer Epic tokens) + let deviceID: String? /// User's display name let displayName: String /// Access token expiration timestamp (ISO 8601 format) @@ -330,7 +330,7 @@ extension Legendary { clientID = try container.decode(String.self, forKey: .clientID) clientService = try container.decode(String.self, forKey: .clientService) - deviceID = try container.decode(String.self, forKey: .deviceID) + deviceID = try container.decodeIfPresent(String.self, forKey: .deviceID) displayName = try container.decode(String.self, forKey: .displayName) let expiresAtString = try container.decode(String.self, forKey: .expiresAt) @@ -371,7 +371,7 @@ extension Legendary { try container.encode(dateFormatter.string(from: authTime), forKey: .authTime) try container.encode(clientID, forKey: .clientID) try container.encode(clientService, forKey: .clientService) - try container.encode(deviceID, forKey: .deviceID) + try container.encodeIfPresent(deviceID, forKey: .deviceID) try container.encode(displayName, forKey: .displayName) try container.encode(dateFormatter.string(from: expiresAt), forKey: .expiresAt) try container.encode(expiresIn, forKey: .expiresIn) diff --git a/Mythic/Utilities/GameManager/Legendary/LegendaryInterface.swift b/Mythic/Utilities/GameManager/Legendary/LegendaryInterface.swift index 4e8be414..b5725ab9 100644 --- a/Mythic/Utilities/GameManager/Legendary/LegendaryInterface.swift +++ b/Mythic/Utilities/GameManager/Legendary/LegendaryInterface.swift @@ -501,6 +501,8 @@ final class Legendary { return String(username) } + // surface legendary's own error (e.g. rejected/expired code) instead of a generic failure + try handleCLIErrorOutput(fromStandardErrorOutput: result.standardError ?? .init()) throw SignInError() } diff --git a/Mythic/Views/Unified/Windows/EpicWebAuthView.swift b/Mythic/Views/Unified/Windows/EpicWebAuthView.swift index 4b9e30fe..14029d63 100644 --- a/Mythic/Views/Unified/Windows/EpicWebAuthView.swift +++ b/Mythic/Views/Unified/Windows/EpicWebAuthView.swift @@ -182,11 +182,17 @@ private struct EpicInterceptorWebView: NSViewRepresentable { return } + // WebKit may wrap JSON responses in its own viewer, so isolate the JSON object itself guard let content = result as? String, - let data = content.data(using: .utf8), + let start = content.firstIndex(of: "{"), + let end = content.lastIndex(of: "}"), + let data = String(content[start...end]).data(using: .utf8), let json = try? JSON(data: data) else { + if let content = result as? String, content.contains("authorizationCode") { + Logger.app.error("Epic sign-in page returned an authorization code, but it couldn't be parsed.") + } return }