Skip to content

Commit 769ea80

Browse files
committed
refactor: browser communication
1 parent 23f4eff commit 769ea80

16 files changed

+708
-103
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Immutable.Passport.Runtime.Tests")]

src/Packages/Passport/Runtime/Scripts/Private/AssemblyInfo.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserCommunicationsManager.cs

Lines changed: 138 additions & 101 deletions
Large diffs are not rendered by default.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Immutable.Passport.Helpers;
2+
using Immutable.Passport.Model;
3+
using UnityEngine;
4+
5+
namespace Immutable.Passport.Core
6+
{
7+
/// <summary>
8+
/// Handles serialization of outgoing requests and deserialization/validation
9+
/// of incoming responses for the game bridge.
10+
/// </summary>
11+
internal static class BrowserMessageCodec
12+
{
13+
/// <summary>
14+
/// Serializes a request into a JavaScript function call string.
15+
/// </summary>
16+
internal static string BuildJsCall(BrowserRequest request)
17+
{
18+
var escapedJson = JsonUtility.ToJson(request).Replace("\\", "\\\\").Replace("\"", "\\\"");
19+
return $"callFunction(\"{escapedJson}\")";
20+
}
21+
22+
/// <summary>
23+
/// Deserializes and validates a raw game bridge response message.
24+
/// </summary>
25+
internal static BrowserResponse ParseAndValidateResponse(string message)
26+
{
27+
var response = message.OptDeserializeObject<BrowserResponse>();
28+
29+
if (response == null || string.IsNullOrEmpty(response.responseFor) || string.IsNullOrEmpty(response.requestId))
30+
{
31+
throw new PassportException("Response from game bridge is incorrect. Check game bridge file.");
32+
}
33+
34+
return response;
35+
}
36+
}
37+
}

src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserMessageCodec.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using Immutable.Passport.Core.Logging;
3+
using Immutable.Passport.Model;
4+
5+
namespace Immutable.Passport.Core
6+
{
7+
/// <summary>
8+
/// Maps game bridge error responses into typed Passport exceptions.
9+
/// </summary>
10+
internal static class BrowserResponseErrorMapper
11+
{
12+
private const string TAG = "[Browser Response Error Mapper]";
13+
14+
/// <summary>
15+
/// Converts a failed BrowserResponse into the appropriate PassportException.
16+
/// </summary>
17+
internal static PassportException MapToException(BrowserResponse response)
18+
{
19+
try
20+
{
21+
if (!string.IsNullOrEmpty(response.error) && !string.IsNullOrEmpty(response.errorType))
22+
{
23+
var type = (PassportErrorType)Enum.Parse(typeof(PassportErrorType), response.errorType);
24+
return new PassportException(response.error, type);
25+
}
26+
27+
return new PassportException(!string.IsNullOrEmpty(response.error) ? response.error : "Unknown error");
28+
}
29+
catch (Exception ex)
30+
{
31+
PassportLogger.Error($"{TAG} Parse passport type error: {ex.Message}");
32+
}
33+
34+
return new PassportException(response.error ?? "Failed to parse error");
35+
}
36+
}
37+
}

src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserResponseErrorMapper.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Collections.Generic;
2+
using Cysharp.Threading.Tasks;
3+
4+
namespace Immutable.Passport.Core
5+
{
6+
/// <summary>
7+
/// Tracks in-flight game bridge requests by mapping request IDs to their completion sources.
8+
/// </summary>
9+
internal class PendingRequestRegistry
10+
{
11+
private readonly Dictionary<string, UniTaskCompletionSource<string>> _requests = new Dictionary<string, UniTaskCompletionSource<string>>();
12+
13+
/// <summary>
14+
/// Registers a new pending request and returns its completion source.
15+
/// </summary>
16+
internal UniTaskCompletionSource<string> Register(string requestId)
17+
{
18+
var completion = new UniTaskCompletionSource<string>();
19+
_requests.Add(requestId, completion);
20+
return completion;
21+
}
22+
23+
/// <summary>
24+
/// Returns true if a pending request exists for the given ID.
25+
/// </summary>
26+
internal bool Contains(string requestId)
27+
{
28+
return _requests.ContainsKey(requestId);
29+
}
30+
31+
/// <summary>
32+
/// Retrieves the completion source for a pending request.
33+
/// </summary>
34+
internal UniTaskCompletionSource<string> Get(string requestId)
35+
{
36+
return _requests[requestId];
37+
}
38+
39+
/// <summary>
40+
/// Removes a completed request from the registry.
41+
/// </summary>
42+
internal void Remove(string requestId)
43+
{
44+
_requests.Remove(requestId);
45+
}
46+
}
47+
}

src/Packages/Passport/Runtime/Scripts/Private/Core/PendingRequestRegistry.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Packages/Passport/Tests/Runtime/Scripts/Core/BrowserCommunicationsManagerTests.cs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Immutable.Browser.Core;
44
using Immutable.Passport.Model;
55
using UnityEngine;
6+
using UnityEngine.TestTools;
67
using System.Text.RegularExpressions;
78
using System.Threading.Tasks;
89
using Immutable.Passport.Helpers;
@@ -78,7 +79,7 @@ public async Task CallAndResponse_Failed_NoRequestId()
7879
}
7980

8081
Assert.NotNull(e);
81-
Assert.IsTrue(e.Message.Contains("Response from browser is incorrect") == true);
82+
Assert.IsTrue(e.Message.Contains("Response from game bridge is incorrect") == true);
8283
}
8384

8485
[Test]
@@ -149,6 +150,91 @@ public void CallAndResponse_Success_BrowserReady()
149150

150151
Assert.True(onReadyCalled);
151152
}
153+
154+
[Test]
155+
public void SetCallTimeout_DoesNotThrow()
156+
{
157+
Assert.DoesNotThrow(() => manager.SetCallTimeout(5000));
158+
}
159+
160+
[Test]
161+
public void LaunchAuthURL_ForwardsUrlAndRedirectUri()
162+
{
163+
manager.LaunchAuthURL("https://auth.example.com", "myapp://callback");
164+
165+
Assert.AreEqual("https://auth.example.com", mockClient.lastLaunchedUrl);
166+
Assert.AreEqual("myapp://callback", mockClient.lastLaunchedRedirectUri);
167+
}
168+
169+
[Test]
170+
public async Task CallAndResponse_Failed_ErrorFieldSet_SuccessTrue_ThrowsException()
171+
{
172+
// success=true but an error field is present — should still be treated as failure
173+
mockClient.browserResponse = new BrowserResponse
174+
{
175+
responseFor = FUNCTION_NAME,
176+
error = ERROR,
177+
success = true
178+
};
179+
180+
PassportException e = null;
181+
try
182+
{
183+
await manager.Call(FUNCTION_NAME);
184+
}
185+
catch (PassportException ex)
186+
{
187+
e = ex;
188+
}
189+
190+
Assert.NotNull(e);
191+
Assert.AreEqual(ERROR, e.Message);
192+
}
193+
194+
[Test]
195+
public void HandleResponse_UnknownRequestId_Throws()
196+
{
197+
// A well-formed response whose requestId was never registered via Call()
198+
var response = new BrowserResponse
199+
{
200+
responseFor = FUNCTION_NAME,
201+
requestId = "unknown-request-id",
202+
success = true
203+
};
204+
205+
LogAssert.Expect(LogType.Error, new Regex("No TaskCompletionSource for request id"));
206+
207+
var ex = Assert.Throws<PassportException>(
208+
() => mockClient.InvokeUnityPostMessage(JsonUtility.ToJson(response))
209+
);
210+
211+
Assert.IsTrue(ex.Message.Contains("No TaskCompletionSource for request id"));
212+
}
213+
214+
[Test]
215+
public async Task CallAndResponse_Failed_ClientError_NoErrorField()
216+
{
217+
// success=false but no error or errorType - should get "Unknown error"
218+
mockClient.browserResponse = new BrowserResponse
219+
{
220+
responseFor = FUNCTION_NAME,
221+
success = false
222+
};
223+
224+
PassportException e = null;
225+
try
226+
{
227+
await manager.Call(FUNCTION_NAME);
228+
}
229+
catch (PassportException ex)
230+
{
231+
e = ex;
232+
}
233+
234+
Assert.NotNull(e);
235+
Assert.Null(e.Type);
236+
Assert.AreEqual("Unknown error", e.Message);
237+
}
152238
}
153239

154240
internal class MockBrowserClient : IWebBrowserClient
@@ -200,9 +286,13 @@ private string Between(string value, string a, string b)
200286
return value.Substring(adjustedPosA, posB - adjustedPosA);
201287
}
202288

289+
public string lastLaunchedUrl;
290+
public string lastLaunchedRedirectUri;
291+
203292
public void LaunchAuthURL(string url, string redirectUri)
204293
{
205-
throw new NotImplementedException();
294+
lastLaunchedUrl = url;
295+
lastLaunchedRedirectUri = redirectUri;
206296
}
207297

208298
public void Dispose()

0 commit comments

Comments
 (0)