using System.Threading.Tasks;
using UnityEngine;
namespace MyInfoMate.Vr.Net
{
///
/// Appairage du casque — item E2 du lot XR-4.
///
/// Le flux est celui de la tablette, à un paramètre près
/// (tablet-app/lib/Screens/Configuration/config_view.dart:274 et son
/// _fetchAppKey) :
///
/// - code PIN → GET /api/instance/app-key → clé d'API + id d'instance
/// - la clé → GET /api/configuration → les configurations de l'instance
/// - POST /api/device avec appType = VR → le casque existe dans la flotte
///
///
/// ⚠️ Le paramètre qui change tout, c'est appType. Sans lui, le serveur
/// crée une tablette (DeviceController.Create, défaut Tablet) : le casque
/// atterrirait dans l'onglet Kiosk du manager. Avec VR, il est rattaché à
/// l'ApplicationInstance VR — et un 404 signifie que le canal VR n'est pas
/// activé sur cette instance.
///
public class PairingService
{
/// Valeur 3 de l'enum AppType côté serveur — persistée en int.
public const int AppTypeVr = 3;
/// Nom de l'enum ApiKeyAppType, envoyé tel quel en query.
const string ApiKeyAppTypeVr = "VrApp";
const string PrefsBaseUrl = "myinfomate.baseUrl";
const string PrefsApiKey = "myinfomate.apiKey";
const string PrefsInstanceId = "myinfomate.instanceId";
const string PrefsDeviceId = "myinfomate.deviceId";
const string PrefsConfigurationId = "myinfomate.configurationId";
public class Pairing
{
public string BaseUrl;
public string ApiKey;
public string InstanceId;
public string DeviceId;
public string ConfigurationId;
}
class AppKeyResponse
{
public string Key;
public string InstanceId;
}
class ConfigurationSummary
{
public string Id;
public string Label;
}
class DeviceResponse
{
public string Id;
public string Identifier;
}
///
/// L'identifiant matériel du casque. SystemInfo.deviceUniqueIdentifier est
/// stable pour une installation donnée — c'est lui que DeviceController.Create
/// utilise pour reconnaître un appareil déjà enregistré au lieu d'en créer un second.
///
public static string HeadsetIdentifier => SystemInfo.deviceUniqueIdentifier;
/// L'appairage précédent, ou null si ce casque n'a jamais été appairé.
public static Pairing Restore()
{
var key = PlayerPrefs.GetString(PrefsApiKey, null);
if (string.IsNullOrEmpty(key)) return null;
return new Pairing
{
BaseUrl = PlayerPrefs.GetString(PrefsBaseUrl, null),
ApiKey = key,
InstanceId = PlayerPrefs.GetString(PrefsInstanceId, null),
DeviceId = PlayerPrefs.GetString(PrefsDeviceId, null),
ConfigurationId = PlayerPrefs.GetString(PrefsConfigurationId, null)
};
}
public static void Forget()
{
foreach (var k in new[] { PrefsBaseUrl, PrefsApiKey, PrefsInstanceId,
PrefsDeviceId, PrefsConfigurationId })
PlayerPrefs.DeleteKey(k);
PlayerPrefs.Save();
}
///
/// Appaire ce casque. vide = la première
/// configuration de l'instance, ce qui suffit à une borne qui n'en a qu'une.
///
public async Task> PairAsync(
string baseUrl, string pinCode, string headsetName, string configurationId = null)
{
var client = new ApiClient(baseUrl);
var keyResult = await client.GetAsync(
$"/api/instance/app-key?pinCode={UnityWebRequestEscape(pinCode)}&appType={ApiKeyAppTypeVr}");
if (!keyResult.Ok) return Fail(keyResult.Error);
if (keyResult.Value == null || string.IsNullOrEmpty(keyResult.Value.Key))
return Fail("Ce code PIN ne correspond à aucun lieu.");
client.ApiKey = keyResult.Value.Key;
var instanceId = keyResult.Value.InstanceId;
if (string.IsNullOrEmpty(configurationId))
{
var configs = await client.GetAsync(
$"/api/configuration?instanceId={instanceId}");
if (!configs.Ok) return Fail(configs.Error);
if (configs.Value == null || configs.Value.Length == 0)
return Fail("Ce lieu n'a encore aucun contenu publié.");
configurationId = configs.Value[0].Id;
}
// Le serveur crée lui-même l'AppConfigurationLink porteur de ce DeviceId :
// c'est ce lien que l'onglet XR du manager affiche comme carte de casque.
var device = await client.PostAsync("/api/device", new
{
identifier = HeadsetIdentifier,
name = headsetName,
instanceId,
configurationId,
appType = AppTypeVr,
connected = true
});
if (!device.Ok)
{
// 404 sur cette route ne veut pas dire « introuvable » au sens courant :
// il n'y a pas d'ApplicationInstance VR sur cette instance.
return Fail(device.Error == "Ce contenu n'existe plus sur le serveur."
? "Le canal VR n'est pas activé pour ce lieu."
: device.Error);
}
var pairing = new Pairing
{
BaseUrl = client.BaseUrl,
ApiKey = client.ApiKey,
InstanceId = instanceId,
DeviceId = device.Value?.Id,
ConfigurationId = configurationId
};
Save(pairing);
return new ApiClient.Result { Value = pairing };
}
static void Save(Pairing pairing)
{
PlayerPrefs.SetString(PrefsBaseUrl, pairing.BaseUrl);
PlayerPrefs.SetString(PrefsApiKey, pairing.ApiKey);
PlayerPrefs.SetString(PrefsInstanceId, pairing.InstanceId);
PlayerPrefs.SetString(PrefsDeviceId, pairing.DeviceId);
PlayerPrefs.SetString(PrefsConfigurationId, pairing.ConfigurationId);
PlayerPrefs.Save();
}
static ApiClient.Result Fail(string error) =>
new ApiClient.Result { Error = error };
static string UnityWebRequestEscape(string value) =>
UnityEngine.Networking.UnityWebRequest.EscapeURL(value);
}
}