From a2780f5c970e3af91b978aeb0321c6bc5c494b3f Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Thu, 9 Jul 2026 09:08:09 +0200 Subject: [PATCH 1/2] feat: Add support for WLL custom sign up fields --- Runtime/Client/LootLockerEndPoints.cs | 1 + Runtime/Game/LootLockerSDKManager.cs | 52 ++++ Runtime/Game/Requests/WhiteLabelRequest.cs | 39 +++ .../PlayMode/WhiteLabelSignUpFieldsTest.cs | 234 ++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 1bbf294f3..5c3388f06 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -43,6 +43,7 @@ public class LootLockerEndPoints // White Label Login [Header("White Label Login")] public static EndPointClass whiteLabelSignUp = new EndPointClass("white-label-login/sign-up", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); + public static EndPointClass whiteLabelSignUpFields = new EndPointClass("white-label-login/sign-up/fields", LootLockerHTTPMethod.GET, LootLockerEnums.LootLockerCallerRole.Base); public static EndPointClass whiteLabelLogin = new EndPointClass("white-label-login/login", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); public static EndPointClass whiteLabelVerifySession = new EndPointClass("white-label-login/verify-session", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); public static EndPointClass whiteLabelRequestPasswordReset = new EndPointClass("white-label-login/request-reset-password", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 75b96fc51..ef19deba3 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -2893,6 +2893,58 @@ public static void WhiteLabelSignUp(string email, string password, Action + /// Create new user using the White Label login system, optionally including answers to custom sign-up fields. + /// Call first to retrieve the fields configured for this game, + /// then pass the player's answers as . + /// White Label platform must be enabled in the web console for this to work. + /// + /// E-mail for the new user + /// Password for the new user + /// + /// Answers to the custom sign-up fields configured in the web console. + /// Each entry must include the metadata_key matching a configured field and the value as a JSON string in value_json. + /// Pass null or an empty array if there are no custom fields. + /// + /// onComplete Action for handling the response of type LootLockerWhiteLabelSignupResponse + public static void WhiteLabelSignUp(string email, string password, LootLockerWhiteLabelCustomFieldValue[] customFields, Action onComplete) + { + if (!CheckInitialized(true)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(null)); + return; + } + + LootLockerWhiteLabelUserRequest input = new LootLockerWhiteLabelUserRequest + { + email = email, + password = password, + custom_fields = customFields + }; + + LootLockerAPIManager.WhiteLabelSignUp(input, onComplete); + } + + /// @ingroup WhiteLabel + /// + /// Retrieve the list of custom sign-up fields configured for this game. + /// Use the returned fields to build a sign-up form, then pass the player's answers to + /// . + /// White Label platform must be enabled in the web console for this to work. + /// + /// onComplete Action for handling the response of type LootLockerWhiteLabelSignUpFieldsResponse + public static void WhiteLabelGetSignUpFields(Action onComplete) + { + if (!CheckInitialized(true)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(null)); + return; + } + + LootLockerAPIManager.WhiteLabelGetSignUpFields(onComplete); + } + /// @ingroup WhiteLabel /// /// Request a password reset email for the given email address. diff --git a/Runtime/Game/Requests/WhiteLabelRequest.cs b/Runtime/Game/Requests/WhiteLabelRequest.cs index 68a2d6b13..fcd2d736c 100644 --- a/Runtime/Game/Requests/WhiteLabelRequest.cs +++ b/Runtime/Game/Requests/WhiteLabelRequest.cs @@ -4,11 +4,29 @@ namespace LootLocker.Requests { + public class LootLockerWhiteLabelCustomFieldValue + { + public string metadata_key { get; set; } + public string value_json { get; set; } + } + + public class LootLockerWhiteLabelCustomField + { + public string question_text { get; set; } + public string metadata_key { get; set; } + public string field_type { get; set; } + public string @params { get; set; } + public bool required { get; set; } + public bool sensitive { get; set; } + public int sort_order { get; set; } + } + public class LootLockerWhiteLabelUserRequest { public string email { get; set; } public string password { get; set; } public bool remember { get; set; } + public LootLockerWhiteLabelCustomFieldValue[] custom_fields { get; set; } } public class LootLockerWhiteLabelVerifySessionRequest @@ -41,6 +59,12 @@ public class LootLockerWhiteLabelLoginResponse : LootLockerWhiteLabelSignupRespo public string SessionToken { get; set; } } + [Serializable] + public class LootLockerWhiteLabelSignUpFieldsResponse : LootLockerResponse + { + public LootLockerWhiteLabelCustomField[] fields { get; set; } + } + [Serializable] public class LootLockerWhiteLabelLoginAndStartSessionResponse : LootLockerResponse { @@ -191,6 +215,21 @@ public static void WhiteLabelRequestAccountVerification(string email, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.whiteLabelSignUpFields; + + if (LootLockerConfig.current.domainKey.Length == 0) + { + LootLockerLogger.Log("Domain key must be set in settings", LootLockerLogger.LogLevel.Error); + onComplete?.Invoke(LootLockerResponseFactory.ClientError("Domain key must be set in settings", null)); + + return; + } + + LootLockerServerRequest.CallAPI(null, endPoint.endPoint, endPoint.httpMethod, null, (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }, useAuthToken: false, callerRole: endPoint.callerRole, additionalHeaders: GetDomainHeaders()); + } + public static Dictionary GetDomainHeaders() { Dictionary headers = new Dictionary(); diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs new file mode 100644 index 000000000..ca6abd604 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs @@ -0,0 +1,234 @@ +using System.Collections; +using LootLocker; +using LootLocker.Requests; +using LootLockerTestConfigurationUtils; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace LootLockerTests.PlayMode +{ + public class WhiteLabelSignUpFieldsTest + { + private LootLockerTestGame gameUnderTest = null; + private LootLockerConfig configCopy = null; + private static int TestCounter = 0; + private bool SetupFailed = false; + + [UnitySetUp] + public IEnumerator Setup() + { + TestCounter++; + configCopy = LootLockerConfig.current; + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} setup #####"); + + if (!LootLockerConfig.ClearSettings()) + { + Debug.LogError("Could not clear LootLocker config"); + } + + LootLockerConfig.current.logLevel = LootLockerLogger.LogLevel.Debug; + + // Create game + bool gameCreationCallCompleted = false; + LootLockerTestGame.CreateGame(testName: this.GetType().Name + TestCounter + " ", onComplete: (success, errorMessage, game) => + { + if (!success) + { + gameCreationCallCompleted = true; + Debug.LogError(errorMessage); + SetupFailed = true; + } + gameUnderTest = game; + gameCreationCallCompleted = true; + }); + yield return new WaitUntil(() => gameCreationCallCompleted); + if (SetupFailed) + { + yield break; + } + gameUnderTest?.SwitchToStageEnvironment(); + + // Enable white label login + bool enableWLCompleted = false; + gameUnderTest?.EnableWhiteLabelLogin((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + SetupFailed = true; + } + enableWLCompleted = true; + }); + yield return new WaitUntil(() => enableWLCompleted); + if (SetupFailed) + { + yield break; + } + + Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Successfully created test game and initialized LootLocker"); + + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####"); + } + + [UnityTearDown] + public IEnumerator TearDown() + { + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} test case #####"); + if (gameUnderTest != null) + { + bool gameDeletionCallCompleted = false; + gameUnderTest.DeleteGame(((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + } + + gameUnderTest = null; + gameDeletionCallCompleted = true; + })); + yield return new WaitUntil(() => gameDeletionCallCompleted); + } + + LootLockerStateData.ClearAllSavedStates(); + + LootLockerConfig.CreateNewSettings(configCopy); + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public IEnumerator GetSignUpFields_WithWhiteLabelEnabled_ReturnsFieldsResponse() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + LootLockerWhiteLabelSignUpFieldsResponse actualResponse = null; + bool getFieldsCallCompleted = false; + LootLockerSDKManager.WhiteLabelGetSignUpFields(response => + { + actualResponse = response; + getFieldsCallCompleted = true; + }); + yield return new WaitUntil(() => getFieldsCallCompleted); + + // Then + Assert.IsTrue(actualResponse.success, "GetSignUpFields returned unsuccessful: " + actualResponse.errorData?.message); + // Fields array should be present (empty if no custom fields configured on this game) + Assert.IsNotNull(actualResponse.fields, "Fields array should not be null"); + } + + // Verifies serialization round-trip for the @params keyword-escaped property + [Test, Category("LootLocker"), Category("LootLockerCI")] + public void CustomField_SerializeDeserialize_HandlesParamsKeywordProperty() + { + // Given — a custom field with the @params property set + var original = new LootLockerWhiteLabelCustomField + { + question_text = "When were you born?", + metadata_key = "birth_date", + field_type = "date", + required = true, + sensitive = false, + sort_order = 1 + }; + + // Assign via the @params property (C# verbatim identifier for the keyword 'params') + original.@params = "{\"min\":\"1900-01-01\",\"max\":\"2026-01-01\"}"; + + // When — serialize to JSON + string json = LootLockerJson.SerializeObject(original); + Debug.Log($"Serialized custom field: {json}"); + + // Then — the @params property serialized as "params" in JSON + Assert.IsTrue(json.Contains("\"params\""), + $"JSON must contain the key \"params\", got:\n{json}"); + Assert.IsTrue(json.Contains("\"min\":\"1900-01-01\""), + $"JSON must contain the nested JSON payload, got:\n{json}"); + + // When — deserialize back + var deserialized = LootLockerJson.DeserializeObject(json); + + // Then — the @params value round-trips + Assert.AreEqual(original.question_text, deserialized.question_text, "question_text should round-trip"); + Assert.AreEqual(original.metadata_key, deserialized.metadata_key, "metadata_key should round-trip"); + Assert.AreEqual(original.field_type, deserialized.field_type, "field_type should round-trip"); + Assert.AreEqual(original.required, deserialized.required, "required should round-trip"); + Assert.AreEqual(original.@params, deserialized.@params, "@params should round-trip through serialize/deserialize"); + Assert.AreEqual(original.sort_order, deserialized.sort_order, "sort_order should round-trip"); + } + + // Verifies serialization of request body with custom_fields array + [Test, Category("LootLocker"), Category("LootLockerCI")] + public void UserRequest_SerializeDeserialize_IncludesCustomFields() + { + // Given + var customFieldValue = new LootLockerWhiteLabelCustomFieldValue + { + metadata_key = "tos_agree", + value_json = "true" + }; + + var request = new LootLockerWhiteLabelUserRequest + { + email = "player@example.com", + password = "s3cur3p4ssw0rd", + remember = false, + custom_fields = new[] { customFieldValue } + }; + + // When + string json = LootLockerJson.SerializeObject(request); + Debug.Log($"Serialized sign-up request: {json}"); + + // Then — verify custom_fields appear in JSON with correct keys + Assert.IsTrue(json.Contains("\"custom_fields\""), + $"JSON must contain \"custom_fields\", got:\n{json}"); + Assert.IsTrue(json.Contains("\"metadata_key\":\"tos_agree\""), + $"JSON must contain metadata_key, got:\n{json}"); + Assert.IsTrue(json.Contains("\"value_json\":\"true\""), + $"JSON must contain value_json, got:\n{json}"); + // Verify existing fields still serialize + Assert.IsTrue(json.Contains("\"email\":\"player@example.com\""), + $"JSON must contain email, got:\n{json}"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public IEnumerator SignUp_WithCustomFields_Succeeds() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // Given — a unique email so we don't conflict with repeated test runs + string email = $"test-{TestCounter}-{System.Guid.NewGuid():N}@example.com"; + string password = "TestPassword123!"; + + LootLockerWhiteLabelCustomFieldValue[] customFields = new LootLockerWhiteLabelCustomFieldValue[] + { + new LootLockerWhiteLabelCustomFieldValue + { + metadata_key = "birth_date", + value_json = "\"2000-01-15\"" + }, + new LootLockerWhiteLabelCustomFieldValue + { + metadata_key = "tos_agree", + value_json = "true" + } + }; + + // When + LootLockerWhiteLabelSignupResponse actualResponse = null; + bool signUpCallCompleted = false; + LootLockerSDKManager.WhiteLabelSignUp(email, password, customFields, response => + { + actualResponse = response; + signUpCallCompleted = true; + }); + yield return new WaitUntil(() => signUpCallCompleted); + + // Then + Assert.IsTrue(actualResponse.success, "WhiteLabelSignUp with custom fields failed: " + actualResponse.errorData?.message); + Assert.IsNotNull(actualResponse.Email, "Email should be present in response"); + } + } +} From 0ad974637f75106c8ed313c933283f48944dd1a7 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 17 Jul 2026 16:25:51 +0200 Subject: [PATCH 2/2] fix: Decouple custom_fields from login request, fix test assertions - Removed custom_fields from LootLockerWhiteLabelUserRequest so login requests no longer serialize an unexpected custom_fields field - Created LootLockerWhiteLabelSignUpRequest inheriting from the base user request and carrying custom_fields - Updated both WhiteLabelSignUp overloads to use the signup-specific request type - Fixed @params JSON escaping assertion to match escaped inner quotes - Removed Debug.Log line that printed the full sign-up request (including password) to CI logs - Fixed assert message wording to describe failure instead of success Addresses review comments on PR #481 --- Runtime/Game/LootLockerSDKManager.cs | 4 ++-- Runtime/Game/Requests/WhiteLabelRequest.cs | 6 +++++- .../PlayMode/WhiteLabelSignUpFieldsTest.cs | 10 +++++----- .../PlayMode/WhiteLabelSignUpFieldsTest.cs.meta | 2 ++ 4 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs.meta diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index ef19deba3..b7c07bff4 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -2884,7 +2884,7 @@ public static void WhiteLabelSignUp(string email, string password, Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }, useAuthToken: false, callerRole: endPoint.callerRole, additionalHeaders: GetDomainHeaders()); } - public static void WhiteLabelSignUp(LootLockerWhiteLabelUserRequest input, Action onComplete) + public static void WhiteLabelSignUp(LootLockerWhiteLabelSignUpRequest input, Action onComplete) { EndPointClass endPoint = LootLockerEndPoints.whiteLabelSignUp; diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs index ca6abd604..c501aee98 100644 --- a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs +++ b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs @@ -66,7 +66,7 @@ public IEnumerator Setup() yield break; } - Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Successfully created test game and initialized LootLocker"); + Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Failed to initialize LootLockerSDK"); Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####"); } @@ -143,8 +143,9 @@ public void CustomField_SerializeDeserialize_HandlesParamsKeywordProperty() // Then — the @params property serialized as "params" in JSON Assert.IsTrue(json.Contains("\"params\""), $"JSON must contain the key \"params\", got:\n{json}"); - Assert.IsTrue(json.Contains("\"min\":\"1900-01-01\""), - $"JSON must contain the nested JSON payload, got:\n{json}"); + // The @params value is a JSON string, so inner quotes will be escaped in the serialized output + Assert.IsTrue(json.Contains("\\\"min\\\""), + $"JSON must contain the escaped nested JSON payload, got:\n{json}"); // When — deserialize back var deserialized = LootLockerJson.DeserializeObject(json); @@ -169,7 +170,7 @@ public void UserRequest_SerializeDeserialize_IncludesCustomFields() value_json = "true" }; - var request = new LootLockerWhiteLabelUserRequest + var request = new LootLockerWhiteLabelSignUpRequest { email = "player@example.com", password = "s3cur3p4ssw0rd", @@ -179,7 +180,6 @@ public void UserRequest_SerializeDeserialize_IncludesCustomFields() // When string json = LootLockerJson.SerializeObject(request); - Debug.Log($"Serialized sign-up request: {json}"); // Then — verify custom_fields appear in JSON with correct keys Assert.IsTrue(json.Contains("\"custom_fields\""), diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs.meta b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs.meta new file mode 100644 index 000000000..7257c52f1 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9a180f9addc1f8a459512a0ef43ef405 \ No newline at end of file