In a MAUI Hybrid Blazor application, a login page may need to do more than validate a username and password. In a real application, we may also need to detect the client's public IP address, check whether that IP is allowed to use the application, keep a few login values in sessionStorage, and finally redirect to the correct module.

This article shows a practical pattern for coordinating those operations with IJSRuntime, OnAfterRenderAsync, and a small JavaScript bridge between the page and Blazor.

๐Ÿงฉ The Problem

The login page has several jobs:

The important part is timing. Browser-dependent JavaScript work should not be forced into OnInitialized when the required DOM or JavaScript state is not ready yet.

โš™๏ธ Step 1: Wait Until the Component Has Rendered

The login page first registers its JavaScript callback in OnAfterRenderAsync:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await RegisterJs();

        await Task.Delay(1000);
        isVideoReady = true;

        await InvokeAsync(StateHasChanged);
    }
}

OnAfterRenderAsync is a better place for browser interop that depends on the rendered page. The first-render check also prevents the registration from running repeatedly.

๐Ÿ”— Step 2: Create a JavaScript-to-Blazor Bridge

A DotNetObjectReference lets JavaScript call a method on the Blazor component:

private DotNetObjectReference<Login_DTVS>? objRef;

protected override void OnInitialized()
{
    objRef = DotNetObjectReference.Create(this);
}

private async Task RegisterJs()
{
    await JS.InvokeVoidAsync(
        "myInterop.register",
        objRef);
}

public void Dispose()
{
    objRef?.Dispose();
}

The JavaScript object keeps the .NET reference:

window.myInterop = {
    dotNetHelper: null,

    register: function (dotNetHelper) {
        this.dotNetHelper = dotNetHelper;
        window.hasregistered = true;
    },

    callBlazor: function () {
        this.dotNetHelper.invokeMethodAsync(
            "UpdateMessage",
            "Hello from JavaScript!"
        );
    }
};

The Dispose method is important because the JavaScript reference points back to the Blazor component. Releasing the DotNetObjectReference helps avoid keeping the component alive longer than necessary.

๐ŸŒ Step 3: Get the Public IP Address

The browser can call a public IP service directly:

async function getPublicIP() {
    try {
        const response = await fetch(
            'https://api.ipify.org?format=json'
        );

        const data = await response.json();

        console.log("Your IP Address is:", data.ip);
        return data.ip;
    }
    catch (error) {
        console.error("Error fetching IP from ipify:", error);
    }
}

Because the request is asynchronous, the calling C# code must await the JavaScript result:

string res = await JS.InvokeAsync<string>("getPublicIP");
clientIp = res;

This is preferable to guessing the public address from the local device network interface, especially when the MAUI application is behind NAT, Wi-Fi routers, corporate networks, or mobile networks.

๐Ÿ›ก๏ธ Step 4: Check Whether the IP Has Access

Once the public IP is known, the Blazor page can call the application's API:

var response = await Http.GetFromJsonAsync<AccessResponse>(
    "http://serverip:serverport/Auth/check-access/" + res);

if (response != null)
{
    hasFullAccess = response.HasAccess;
    clientIp = response.IpAddress;
}

A simple response model can represent the result:

public class AccessResponse
{
    public bool HasAccess { get; set; }
    public string IpAddress { get; set; }
}

The result can then control the login UI:

<button class="login-btn"
        @onclick="HandleLogin"
        disabled="@isLoading || !@hasFullAccess">
    Sign in
</button>

This creates a simple gate: the user can see the login screen, but authentication controls remain unavailable until the access check completes successfully.

๐Ÿ’พ Step 5: Store Login Context in sessionStorage

After validation, the selected plant and login ID can be saved in browser session storage:

await JS.InvokeVoidAsync(
    "sessionStorage.setItem",
    "PlantCode",
    selectedPlant);

await JS.InvokeVoidAsync(
    "sessionStorage.setItem",
    "LoginId",
    username);

await JS.InvokeVoidAsync(
    "sessionStorage.setItem",
    "CompCode",
    selectedPlant);

Only non-sensitive state should be persisted this way. In particular, the example deliberately does not store the password in sessionStorage.

๐Ÿ” Step 6: Perform the Login API Request

The request can contain the selected plant, credentials, and public IP:

var reqq = new AuthApiService.LoginRequest
{
    category = selectedPlant == "1108" ? "E" : "S",
    UserID = username,
    LoginPwd = password,
    IpAddress = clientIp
};

var lr = await AuthService.LoginAsync(reqq);

The response can carry additional session information such as employee number, supplier code, photo path, focus target, or the destination module.

๐Ÿšฆ Step 7: Handle Errors Before Redirecting

A login API can return an error message instead of a redirect target:

if (!string.IsNullOrWhiteSpace(lr.Error))
{
    ToastMessage = lr.Error;
    ShowToast = true;

    StateHasChanged();
    await Task.Delay(3000);

    ShowToast = false;
    return;
}

Keeping this branch before the final navigation makes the login experience much easier to understand for the user.

โžก๏ธ Step 8: Redirect to the Target Module

When the API provides a valid destination, Blazor navigation can take over:

if (!string.IsNullOrWhiteSpace(lr.RedirectTo))
{
    await AuthService.AutoRejectPendingApprovalsAsync(selectedPlant);
    Nav.NavigateTo("/" + lr.RedirectTo);
}

This keeps the login page responsible for authentication while allowing the server to decide which module should open next.

๐Ÿง  Why the JavaScript Timing Matters

One of the easiest mistakes in a MAUI Hybrid Blazor application is to assume that every JavaScript call can be executed from OnInitialized. Browser APIs and DOM elements may not be available at that point.

The practical sequence is:

  1. Blazor constructs the component.
  2. The component renders.
  3. OnAfterRenderAsync(firstRender: true) runs.
  4. JavaScript interop is registered.
  5. JavaScript waits until the bridge exists.
  6. JavaScript calls the Blazor method.
  7. Blazor gets the public IP and performs the access check.
  8. The login controls become available when access is granted.

This approach avoids trying to call browser-only functionality before the page is ready.

๐Ÿงน Cleanup Matters

Because a DotNetObjectReference is created, the component should release it:

public void Dispose()
{
    objRef?.Dispose();
}

This is especially useful for pages that can be entered and left multiple times during the lifetime of the application.

โœ… Final Pattern

The complete flow can be summarized as:

Page renders
    โ†“
OnAfterRenderAsync(firstRender)
    โ†“
Register JavaScript interop
    โ†“
JavaScript calls Blazor
    โ†“
Fetch public IP
    โ†“
API checks IP access
    โ†“
Enable login controls
    โ†“
Authenticate user
    โ†“
sessionStorage stores non-sensitive context
    โ†“
Navigate to the target module

This pattern works well for MAUI Hybrid applications where Blazor provides the UI, JavaScript supplies browser APIs, and an ASP.NET Core API remains responsible for authentication and server-side authorization decisions.

๐Ÿš€ Conclusion

MAUI Hybrid Blazor gives you a useful middle ground: you can build the entire login UI with Razor components while still using JavaScript interop for browser features such as public IP detection and session storage.

The key is to respect the component lifecycle. Register browser-dependent JavaScript after the first render, use a controlled callback between JavaScript and .NET, await asynchronous calls, avoid storing passwords in browser storage, and dispose of the .NET object reference when the component is finished.

Written for AlgoLassi โ€” practical .NET, Blazor, MAUI, SQL Server, and developer tutorials.

๐Ÿ’ฌ Comments

Sign in with Google to publish immediately, or comment anonymously and wait for approval.

Loading comments...