unit View.DeviceRegistration;

interface

uses
  System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
  Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.JSON,
  JS, XData.Web.Connection, WEBLib.ExtCtrls,
  App.Types, ConnectionModule, XData.Web.Client;

type
  TFViewDeviceRegistration = class(TWebForm)
    edtDeviceName: TWebEdit;
    btnRegister: TWebButton;
    pnlMessage: TWebPanel;
    lblMessage: TWebLabel;
    btnCloseNotification: TWebButton;
    XDataWebClient: TXDataWebClient;
    procedure btnRegisterClick(Sender: TObject);
    procedure btnCloseNotificationClick(Sender: TObject);
    procedure WebFormCreate(Sender: TObject);
  private
    FRegistrationProc: TSuccessProc;
    procedure ShowNotification(const AMsg: string; AIsError: Boolean = True);
    procedure HideNotification;
    procedure SetBusy(ABusy: Boolean);
    procedure DoWebAuthnCreate(ADeviceName, AChallenge, AChallengeToken: string);
  public
    class procedure Display(ARegistrationProc: TSuccessProc);
  end;

var
  FViewDeviceRegistration: TFViewDeviceRegistration;

implementation

uses
  Auth.Service,
  View.ErrorPage;

{$R *.dfm}

class procedure TFViewDeviceRegistration.Display(ARegistrationProc: TSuccessProc);
begin
  if Assigned(FViewDeviceRegistration) then
    FViewDeviceRegistration.Free;
  FViewDeviceRegistration := TFViewDeviceRegistration.CreateNew;
  FViewDeviceRegistration.FRegistrationProc := ARegistrationProc;
end;

procedure TFViewDeviceRegistration.WebFormCreate(Sender: TObject);
var
  userAgent: string;
begin
  HideNotification;
  userAgent := '';
  asm
    userAgent = navigator.userAgent;
  end;
  asm
    var el = document.getElementById('view.devicereg.useragent');
    if (el) el.textContent = userAgent;
  end;
end;

procedure TFViewDeviceRegistration.SetBusy(ABusy: Boolean);
begin
  asm
    var btn = document.getElementById('view.devicereg.btnregister');
    if (btn) {
      btn.disabled = ABusy;
      btn.textContent = ABusy ? 'Waiting for authenticator...' : 'Register This Device';
    }
  end;
end;

procedure TFViewDeviceRegistration.btnRegisterClick(Sender: TObject);
var
  deviceName: string;

  procedure OnBeginOK(AChallenge, AChallengeToken: string);
  begin
    DoWebAuthnCreate(deviceName, AChallenge, AChallengeToken);
  end;

  procedure OnBeginError(AMsg: string);
  begin
    SetBusy(False);
    ShowNotification(AMsg);
  end;

begin
  deviceName := Trim(edtDeviceName.Text);
  if deviceName = '' then
    deviceName := 'Unnamed Device';

  SetBusy(True);
  HideNotification;

  AuthService.BeginRegistration(deviceName, @OnBeginOK, @OnBeginError);
end;

procedure TFViewDeviceRegistration.DoWebAuthnCreate(
  ADeviceName, AChallenge, AChallengeToken: string);
var
  deviceName, challenge, challengeToken: string;

  procedure OnCompleteOK;
  begin
    // Registration complete — proceed to login
    FRegistrationProc;
  end;

  procedure OnCompleteError(AMsg: string);
  begin
    SetBusy(False);
    ShowNotification(AMsg);
  end;

  procedure OnCredential(ACredentialId, AAttestationObject, AClientDataJSON: string);
  begin
    AuthService.CompleteRegistration(
      deviceName, ACredentialId, AAttestationObject, AClientDataJSON, challengeToken,
      @OnCompleteOK, @OnCompleteError
    );
  end;

  procedure OnWebAuthnError(AMsg: string);
  begin
    SetBusy(False);
    ShowNotification(AMsg);
  end;

begin
  deviceName     := ADeviceName;
  challenge      := AChallenge;
  challengeToken := AChallengeToken;

  // Call navigator.credentials.create() via WebAuthn API
  asm
    (function() {
      // Decode base64url challenge to Uint8Array
      function b64urlToArr(b64) {
        b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
        while (b64.length % 4) b64 += '=';
        var bin = atob(b64);
        var arr = new Uint8Array(bin.length);
        for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
        return arr;
      }
      // Encode ArrayBuffer to base64url
      function arrToB64url(buf) {
        var bin = String.fromCharCode.apply(null, new Uint8Array(buf));
        return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
      }

      // Random 16-byte user handle (not linked to user identity for platform keys)
      var userId = new Uint8Array(16);
      crypto.getRandomValues(userId);

      navigator.credentials.create({
        publicKey: {
          challenge: b64urlToArr(challenge),
          rp: { id: window.location.hostname, name: 'emiMobile' },
          user: { id: userId, name: 'emimobile-user', displayName: 'emiMobile User' },
          pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
          timeout: 60000,
          attestation: 'none',
          authenticatorSelection: {
            userVerification: 'required',
            residentKey: 'preferred'
          }
        }
      }).then(function(cred) {
        var credId  = arrToB64url(cred.rawId);
        var attObj  = arrToB64url(cred.response.attestationObject);
        var cdJson  = arrToB64url(cred.response.clientDataJSON);
        OnCredential(credId, attObj, cdJson);
      }).catch(function(err) {
        OnWebAuthnError('WebAuthn error: ' + err.message);
      });
    })();
  end;
end;

procedure TFViewDeviceRegistration.btnCloseNotificationClick(Sender: TObject);
begin
  HideNotification;
end;

procedure TFViewDeviceRegistration.ShowNotification(const AMsg: string; AIsError: Boolean);
begin
  if AMsg <> '' then
  begin
    lblMessage.Caption := AMsg;
    asm
      var el = document.getElementById('view.devicereg.message');
      if (el) {
        el.classList.remove('alert-danger', 'alert-warning');
        el.classList.add(AIsError ? 'alert-danger' : 'alert-warning');
        el.classList.remove('d-none');
      }
    end;
  end;
end;

procedure TFViewDeviceRegistration.HideNotification;
begin
  asm
    var el = document.getElementById('view.devicereg.message');
    if (el) el.classList.add('d-none');
  end;
end;

end.
