Skip to content
Open
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
23 changes: 13 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
# dotenv files
.env

# Local MAUI mobile configuration overrides
src/NoteBookmark.MauiApp/wwwroot/appsettings.Development.json

# User-specific files
*.rsuser
*.suo
Expand Down Expand Up @@ -510,15 +513,15 @@ todos/
.copilot/

# Squad/Agent files
.github/agents/
# Squad (local AI team - not committed)
.ai-team/
.github/agents/
# Squad (local AI team - not committed)
.ai-team/

src/NoteBookmark.BlazorApp/Data/
# Squad: ignore runtime state (logs, inbox, sessions)
.squad/orchestration-log/
.squad/log/
.squad/decisions/inbox/
.squad/sessions/
# Squad: SubSquad activation file (local to this machine)
.squad-workstream
# Squad: ignore runtime state (logs, inbox, sessions)
.squad/orchestration-log/
.squad/log/
.squad/decisions/inbox/
.squad/sessions/
# Squad: SubSquad activation file (local to this machine)
.squad-workstream
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
<!-- MAUI packages -->
<PackageVersion Include="Microsoft.Maui.Controls" Version="10.0.20" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.20" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<!-- Other packages -->
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="Markdig" Version="0.44.0" />
Expand Down
15 changes: 15 additions & 0 deletions NoteBookmark.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.BlazorApp.Test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.SharedUI", "src\NoteBookmark.SharedUI\NoteBookmark.SharedUI.csproj", "{1AD790B0-8C91-468A-B21E-C2C5A4F7E1CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.MauiApp", "src\NoteBookmark.MauiApp\NoteBookmark.MauiApp.csproj", "{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -155,6 +157,18 @@ Global
{1AD790B0-8C91-468A-B21E-C2C5A4F7E1CA}.Release|x64.Build.0 = Release|Any CPU
{1AD790B0-8C91-468A-B21E-C2C5A4F7E1CA}.Release|x86.ActiveCfg = Release|Any CPU
{1AD790B0-8C91-468A-B21E-C2C5A4F7E1CA}.Release|x86.Build.0 = Release|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Debug|x64.ActiveCfg = Debug|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Debug|x64.Build.0 = Debug|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Debug|x86.ActiveCfg = Debug|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Debug|x86.Build.0 = Debug|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Release|Any CPU.Build.0 = Release|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Release|x64.ActiveCfg = Release|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Release|x64.Build.0 = Release|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Release|x86.ActiveCfg = Release|Any CPU
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -163,6 +177,7 @@ Global
{13B6E1BC-4B32-4082-A080-FE443F598967} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C04232AF-A144-47C9-B4D4-3259C61E5ABC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{1AD790B0-8C91-468A-B21E-C2C5A4F7E1CA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{9FFD0186-2C97-40F7-9CA1-EDEC3C2A0EF3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D59FFF09-97C3-47EF-B64D-B014BFA22C80}
Expand Down
200 changes: 200 additions & 0 deletions docs/maui-android-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Run The .NET MAUI Android App

This guide explains how to run the Android version of the .NET MAUI app and how to configure its local settings.

## 1. What This Project Supports

The MAUI project is at `src/NoteBookmark.MauiApp`.

On Linux, this project is configured to build the Android target only.

## 2. Prerequisites

Install the following first:

- .NET 10 SDK
- .NET MAUI Android workload (`maui-android`)
- .NET WebAssembly tools workload (`wasm-tools`) — also required by the MAUI project
- Android SDK and emulator or a connected Android device

If the workloads are not installed yet, run with `sudo` because the dotnet SDK is installed system-wide:

```bash
sudo dotnet workload install maui-android wasm-tools
```

If Android API dependencies are missing, run this once:

```bash
dotnet build src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj \
-t:InstallAndroidDependencies \
-f net10.0-android \
-p:AndroidSdkDirectory=/home/frank/Android/Sdk \
-p:AcceptAndroidSDKLicenses=True
```

## 3. Configure Keycloak For Mobile

The MAUI app reads only these keys from the `Keycloak` section:

- `Authority`
- `ClientId`
- `RedirectUri`

The code does not read or send a client secret.

That is intentional: the Android app uses Authorization Code Flow with PKCE as a public client. For Keycloak, this means:

- Create or use a public OIDC client for the mobile app
- Disable client authentication for that mobile client
- Enable standard flow
- Set the redirect URI to `notebookmark://auth/callback`

Do not reuse the confidential web client if that client requires a secret. The safer setup is a separate mobile client such as `notebookmark-mobile`.

If you still need the web setup, keep using the confidential client described in `docs/keycloak-setup.md` for the web app, and use a separate public client for MAUI.

## 4. Configure The App

The committed base config is:

- `src/NoteBookmark.MauiApp/wwwroot/appsettings.json`

For local development overrides, create this file:

- `src/NoteBookmark.MauiApp/wwwroot/appsettings.Development.json`

In `DEBUG` builds, the app now loads `appsettings.Development.json` if it exists. That file is gitignored, so it can contain your local Android settings without being committed.

Example:

```json
{
"Keycloak": {
"Authority": "https://keycloak.example.com/realms/notebookmark",
"ClientId": "notebookmark-mobile",
"RedirectUri": "notebookmark://auth/callback"
}
}
```

Notes:

- The `RedirectUri` must match the callback registered in Keycloak
- The same callback is already declared in `Platforms/Android/AndroidManifest.xml`
- If Keycloak or the API is running on your development machine, Android emulator access usually needs `10.0.2.2` instead of `localhost`

## 5. Start Required Services

Before launching the app, make sure the services it depends on are reachable from the emulator or device:

- Keycloak
- NoteBookmark backend services you want to use

If you are using the existing local container setup, start from these docs:

- `docs/keycloak-container-setup.md`
- `docs/docker-compose-deployment.md`

## 6. Start An Emulator Or Connect A Device

If you use a physical device, connect it with USB, enable developer options + USB debugging, and authorize your machine when prompted.

If you use an emulator on Linux, run these commands:

```bash
/home/frank/Android/Sdk/emulator/emulator -list-avds
```

If needed, start the known profile from this repo:

```bash
/home/frank/Android/Sdk/emulator/emulator -avd NB_Android_35
```

If `emulator` is not found, either use the full path above or add it to PATH:

```bash
export ANDROID_SDK_ROOT=/home/frank/Android/Sdk
export PATH=$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$PATH
```

Before running MAUI, verify that Android sees a device:

```bash
adb devices -l
```

You should see at least one entry like `emulator-5554 device ...`.

## 7. Run The MAUI App

From the repository root, run:

```bash
dotnet build src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj \
-t:Run \
-f net10.0-android \
-v minimal
```

This builds the Android target and launches it on the active emulator or device.

Optional: if you have multiple devices connected, list them first:

```bash
adb devices
```

Then target one device explicitly:

```bash
dotnet build src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj \
-t:Run \
-f net10.0-android \
-p:AndroidDeviceUserId=emulator-5554 \
-v minimal
```

## 8. Troubleshooting

If run fails with `error XA0010: No available device`:

- No emulator/device is connected yet
- Start an emulator (section 6) and wait until it fully boots
- Re-run `adb devices -l` and confirm at least one `device` state (not `offline`)
- Re-run the MAUI run command

If `emulator: command not found`:

- Use `/home/frank/Android/Sdk/emulator/emulator` directly
- Or add Android SDK emulator and platform-tools folders to PATH (section 6)

If sign-in fails with `invalid_client` or a similar token error:

- The configured Keycloak client is probably confidential
- Switch the MAUI app to a public client with no secret

If the browser returns from Keycloak but the app does not complete sign-in:

- Verify the redirect URI is exactly `notebookmark://auth/callback`
- Verify the Keycloak client allows that redirect URI

If your local override file is ignored at runtime:

- Make sure the file name is exactly `appsettings.Development.json`
- Run a `Debug` build
- Rebuild the app after creating or changing the file

If the build breaks after a .NET SDK update with errors like `NETSDK1147` (missing workload) or `MSB4019` (missing `.targets` file):

- A SDK update can require new or updated workload manifests that need `sudo` to install
- Run the following two commands to restore and repair:

```bash
sudo dotnet workload restore src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj
sudo dotnet workload repair
```

- The `wasm-tools` workload must be installed in addition to `maui-android` (see Prerequisites)
- If errors persist about missing `.targets` files in the Android SDK pack, `dotnet workload repair` will reinstall the pack and restore the missing files
17 changes: 17 additions & 0 deletions src/NoteBookmark.MauiApp/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:NoteBookmark.MauiApp"
x:Class="NoteBookmark.MauiApp.App">
<Application.Resources>
<ResourceDictionary>

<!--
For information about styling .NET MAUI pages
please refer to the documentation:
https://go.microsoft.com/fwlink/?linkid=2282329
-->

</ResourceDictionary>
</Application.Resources>
</Application>
25 changes: 25 additions & 0 deletions src/NoteBookmark.MauiApp/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using NoteBookmark.MauiApp.Auth;

namespace NoteBookmark.MauiApp;

public partial class App : Application
{
private readonly IAuthService _authService;

public App(IAuthService authService)
{
InitializeComponent();
_authService = authService;
}

protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new MainPage()) { Title = "NoteBookmark" };
}

protected override async void OnStart()
{
base.OnStart();
await _authService.InitializeAsync();
}
}
31 changes: 31 additions & 0 deletions src/NoteBookmark.MauiApp/Auth/IAuthService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace NoteBookmark.MauiApp.Auth;

public interface IAuthService
{
/// <summary>Loads persisted tokens and attempts silent refresh if needed.</summary>
Task InitializeAsync();

/// <summary>Launches the browser-based OIDC login flow.</summary>
Task LoginAsync();

/// <summary>Clears tokens and logs the user out.</summary>
Task LogoutAsync();

/// <summary>
/// Returns a valid access token. Silently refreshes if expired.
/// Returns null when offline with an expired token.
/// </summary>
Task<string?> GetAccessTokenAsync();

/// <summary>True when a non-expired token (or refresh-able token) is available.</summary>
Task<bool> IsAuthenticatedAsync();

/// <summary>Username decoded from the stored JWT, or null if not authenticated.</summary>
string? Username { get; }

/// <summary>
/// Raised when auth state changes (login, logout, session-expired).
/// The bool payload is true when the user is authenticated.
/// </summary>
event EventHandler<bool> AuthStateChanged;
}
Loading
Loading