Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Runtime/Client/LootLockerEndPoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
54 changes: 53 additions & 1 deletion Runtime/Game/LootLockerSDKManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2884,7 +2884,7 @@ public static void WhiteLabelSignUp(string email, string password, Action<LootLo
return;
}

LootLockerWhiteLabelUserRequest input = new LootLockerWhiteLabelUserRequest
LootLockerWhiteLabelSignUpRequest input = new LootLockerWhiteLabelSignUpRequest
{
email = email,
password = password
Expand All @@ -2893,6 +2893,58 @@ public static void WhiteLabelSignUp(string email, string password, Action<LootLo
LootLockerAPIManager.WhiteLabelSignUp(input, onComplete);
}

/// @ingroup WhiteLabel
/// <summary>
/// Create new user using the White Label login system, optionally including answers to custom sign-up fields.
/// Call <see cref="WhiteLabelGetSignUpFields"/> first to retrieve the fields configured for this game,
/// then pass the player's answers as <paramref name="customFields"/>.
/// White Label platform must be enabled in the web console for this to work.
/// </summary>
/// <param name="email">E-mail for the new user</param>
/// <param name="password">Password for the new user</param>
/// <param name="customFields">
/// Answers to the custom sign-up fields configured in the web console.
/// Each entry must include the <c>metadata_key</c> matching a configured field and the value as a JSON string in <c>value_json</c>.
/// Pass null or an empty array if there are no custom fields.
/// </param>
/// <param name="onComplete">onComplete Action for handling the response of type LootLockerWhiteLabelSignupResponse</param>
public static void WhiteLabelSignUp(string email, string password, LootLockerWhiteLabelCustomFieldValue[] customFields, Action<LootLockerWhiteLabelSignupResponse> onComplete)
{
if (!CheckInitialized(true))
{
onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError<LootLockerWhiteLabelSignupResponse>(null));
return;
}

LootLockerWhiteLabelSignUpRequest input = new LootLockerWhiteLabelSignUpRequest
{
email = email,
password = password,
custom_fields = customFields
};

LootLockerAPIManager.WhiteLabelSignUp(input, onComplete);
}

/// @ingroup WhiteLabel
/// <summary>
/// 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
/// <see cref="WhiteLabelSignUp(string, string, LootLockerWhiteLabelCustomFieldValue[], Action{LootLockerWhiteLabelSignupResponse})"/>.
/// White Label platform must be enabled in the web console for this to work.
/// </summary>
/// <param name="onComplete">onComplete Action for handling the response of type LootLockerWhiteLabelSignUpFieldsResponse</param>
public static void WhiteLabelGetSignUpFields(Action<LootLockerWhiteLabelSignUpFieldsResponse> onComplete)
{
if (!CheckInitialized(true))
{
onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError<LootLockerWhiteLabelSignUpFieldsResponse>(null));
return;
}

LootLockerAPIManager.WhiteLabelGetSignUpFields(onComplete);
}

/// @ingroup WhiteLabel
/// <summary>
/// Request a password reset email for the given email address.
Expand Down
45 changes: 44 additions & 1 deletion Runtime/Game/Requests/WhiteLabelRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,35 @@

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 class LootLockerWhiteLabelSignUpRequest : LootLockerWhiteLabelUserRequest
{
public LootLockerWhiteLabelCustomFieldValue[] custom_fields { get; set; }
}

public class LootLockerWhiteLabelVerifySessionRequest
{
public string email { get; set; }
Expand Down Expand Up @@ -41,6 +63,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
{
Expand Down Expand Up @@ -120,7 +148,7 @@ public static void WhiteLabelVerifySession(LootLockerWhiteLabelVerifySessionRequ
LootLockerServerRequest.CallAPI(null, endPoint.endPoint, endPoint.httpMethod, json, (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }, useAuthToken: false, callerRole: endPoint.callerRole, additionalHeaders: GetDomainHeaders());
}

public static void WhiteLabelSignUp(LootLockerWhiteLabelUserRequest input, Action<LootLockerWhiteLabelSignupResponse> onComplete)
public static void WhiteLabelSignUp(LootLockerWhiteLabelSignUpRequest input, Action<LootLockerWhiteLabelSignupResponse> onComplete)
{
EndPointClass endPoint = LootLockerEndPoints.whiteLabelSignUp;

Expand Down Expand Up @@ -191,6 +219,21 @@ public static void WhiteLabelRequestAccountVerification(string email, Action<Loo
LootLockerServerRequest.CallAPI(null, endPoint.endPoint, endPoint.httpMethod, json, onComplete, useAuthToken: false, callerRole: endPoint.callerRole, additionalHeaders: GetDomainHeaders());
}

public static void WhiteLabelGetSignUpFields(Action<LootLockerWhiteLabelSignUpFieldsResponse> 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<LootLockerWhiteLabelSignUpFieldsResponse>("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<string, string> GetDomainHeaders()
{
Dictionary<string, string> headers = new Dictionary<string, string>();
Expand Down
234 changes: 234 additions & 0 deletions Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs
Original file line number Diff line number Diff line change
@@ -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(), "Failed to initialize LootLockerSDK");

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}");
// 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<LootLockerWhiteLabelCustomField>(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 LootLockerWhiteLabelSignUpRequest
{
email = "player@example.com",
password = "s3cur3p4ssw0rd",
remember = false,
custom_fields = new[] { customFieldValue }
};

// When
string json = LootLockerJson.SerializeObject(request);

// 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");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading