From c64d11e1988057d22da28cd4004fe88f322669ed Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 23:00:04 +0100 Subject: [PATCH 1/2] WIP app invite: scaffold MAUI Essentials dependency in shared PostIt Adds Microsoft.Maui.Essentials package and true to src/PostIt/PostIt/PostIt.csproj so the shared project can compile code that calls MAUI Essentials APIs (Microsoft.Maui.ApplicationModel.*). Also adds a draft ContactService that wraps Contacts.Default.GetAllAsync() behind a runtime platform check and permission request. WIP caveats: - The portable MAUI Essentials facade compiles on net10.0 but throws NotImplementedInReferenceAssemblyException at runtime when no platform-specific MAUI Essentials binary is loaded. A PostIt.Android project (or equivalent) must reference the Android MAUI Essentials implementation for Contacts.Default.GetAllAsync() to actually work. - On desktop (Linux/macOS/Windows) the API is unsupported by design; ContactService currently throws PlatformNotSupportedException. A desktop stub returning Array.Empty() is the likely next step. - No tests yet. The scaffold is unverified at runtime; build passes. --- src/PostIt/Directory.Packages.props | 1 + src/PostIt/PostIt/PostIt.csproj | 5 ++- src/PostIt/PostIt/Services/ContactService.cs | 39 ++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/PostIt/PostIt/Services/ContactService.cs diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 900f1428..62b3a343 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index d9cf96d3..1b14653a 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -3,11 +3,13 @@ net10.0 enable latest + true true 1.0.1.0 1.0.1.0 1.0.1-5+Branch.main.Sha.0617fc6bda7151c70559d87177e2dcfb1b60995f 1.0.1-5 + @@ -24,6 +26,7 @@ + @@ -39,4 +42,4 @@ - \ No newline at end of file + diff --git a/src/PostIt/PostIt/Services/ContactService.cs b/src/PostIt/PostIt/Services/ContactService.cs new file mode 100644 index 00000000..bc71d11b --- /dev/null +++ b/src/PostIt/PostIt/Services/ContactService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Maui.ApplicationModel.Communication; +using Microsoft.Maui.ApplicationModel; +using Microsoft.Maui.Devices; + +public class ContactService +{ + public async Task> GetDeviceContactsAsync() + { + // 1. Ensure the platform supports MAUI Essentials APIs + if (DeviceInfo.Current.Platform == DevicePlatform.Unknown) + { + throw new PlatformNotSupportedException("MAUI Essentials is not available on this platform."); + } + + try + { + // 2. Request runtime permission (Required for Android & iOS) + var status = await Permissions.RequestAsync(); + if (status != PermissionStatus.Granted) + { + // Permission denied by user + return Array.Empty(); + } + + // 3. Fetch all contacts + var contactsEnumerable = await Contacts.Default.GetAllAsync(); + return contactsEnumerable ?? Array.Empty(); + } + catch (Exception ex) + { + // Handle cross-platform exceptions or logs here + System.Diagnostics.Debug.WriteLine($"Error fetching contacts: {ex.Message}"); + return Array.Empty(); + } + } +} From 6e5355afeafcd58828be0b0a4c2ba571a665d957 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 23:13:35 +0100 Subject: [PATCH 2/2] feat(app-invite): isolate ContactService to mobile targets Splits the single ContactService class (which threw PlatformNotSupportedException on non-Android/iOS targets) into a platform-conditional structure: - IContactService + ContactDto: shared abstraction in src/PostIt/PostIt/Services/IContactService.cs. ViewModels depend on this; concrete providers map their native shapes to ContactDto. - ContactService.Mobile.cs: MAUI Essentials implementation, compiled only when ANDROID or IOS is defined. Wraps Contacts.Default.GetAllAsync() with permission handling and a NotImplementedInReferenceAssemblyException safety net. - ContactService.Desktop.cs: stub returning an empty list, compiled when neither ANDROID nor IOS is defined. Replaces the 'throw PlatformNotSupportedException' path so desktop targets (PostIt.Desktop, PostIt.Browser) build and run cleanly. The Microsoft.Maui.Essentials portable facade is referenced from PostIt.csproj, but it only becomes functional when the host application project (PostIt.Android, future PostIt.iOS) also references the platform-specific implementation. No tests added: per AGENTS.md, a 'stub returns empty list' test on PostIt.Tests (net10.0 desktop target) would be cosmetic and not detect the real failure mode. Android-side tests require a working PostIt.Android project, which doesn't exist yet. Future providers (Google Contacts API, Exchange, CardDAV) plug in as additional IContactService implementations selected by DI configuration. --- .../PostIt/Services/ContactService.Desktop.cs | 27 ++++++++ .../PostIt/Services/ContactService.Mobile.cs | 64 +++++++++++++++++++ src/PostIt/PostIt/Services/ContactService.cs | 39 ----------- src/PostIt/PostIt/Services/IContactService.cs | 29 +++++++++ 4 files changed, 120 insertions(+), 39 deletions(-) create mode 100644 src/PostIt/PostIt/Services/ContactService.Desktop.cs create mode 100644 src/PostIt/PostIt/Services/ContactService.Mobile.cs delete mode 100644 src/PostIt/PostIt/Services/ContactService.cs create mode 100644 src/PostIt/PostIt/Services/IContactService.cs diff --git a/src/PostIt/PostIt/Services/ContactService.Desktop.cs b/src/PostIt/PostIt/Services/ContactService.Desktop.cs new file mode 100644 index 00000000..82746c49 --- /dev/null +++ b/src/PostIt/PostIt/Services/ContactService.Desktop.cs @@ -0,0 +1,27 @@ +#if !ANDROID && !IOS +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PostIt.Services; + +/// +/// Desktop stub for IContactService. +/// +/// On desktop targets (Linux, macOS, Windows) MAUI Essentials +/// Contacts.Default throws NotImplementedInReferenceAssemblyException, +/// so we short-circuit with an empty list rather than trying to +/// call into the portable facade at runtime. +/// +/// Future provider plug-ins (Google Contacts API, Exchange EWS, +/// CardDAV) can either replace this stub on a per-OS basis or +/// live behind their own IContactService implementation that the +/// DI container selects by configuration. +/// +public sealed class ContactService : IContactService +{ + public Task> GetDeviceContactsAsync(CancellationToken ct = default) + => Task.FromResult>(Array.Empty()); +} +#endif diff --git a/src/PostIt/PostIt/Services/ContactService.Mobile.cs b/src/PostIt/PostIt/Services/ContactService.Mobile.cs new file mode 100644 index 00000000..744fb9a6 --- /dev/null +++ b/src/PostIt/PostIt/Services/ContactService.Mobile.cs @@ -0,0 +1,64 @@ +#if ANDROID || IOS +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Maui.ApplicationModel.Communication; +using Microsoft.Maui.ApplicationModel; +using Microsoft.Maui.Devices; + +namespace PostIt.Services; + +/// +/// Mobile implementation backed by MAUI Essentials Contacts.Default. +/// +/// Compiled only for ANDROID and IOS. On desktop targets, see +/// ContactService.Desktop.cs (the stub that wins at compile time). +/// +/// Note: at runtime, this class throws +/// NotImplementedInReferenceAssemblyException unless the host +/// application project also references the platform-specific +/// Microsoft.Maui.Essentials implementation (typically the +/// PostIt.Android project). On iOS the same is required via +/// PostIt.iOS. On desktop the stub is used and this file is excluded. +/// +public sealed class ContactService : IContactService +{ + public async Task> GetDeviceContactsAsync(CancellationToken ct = default) + { + if (DeviceInfo.Current.Platform == DevicePlatform.Unknown) + return Array.Empty(); + + try + { + var status = await Permissions.RequestAsync(); + if (status != PermissionStatus.Granted) + return Array.Empty(); + + var contacts = await Contacts.Default.GetAllAsync(); + if (contacts is null) return Array.Empty(); + + var result = new List(); + foreach (var c in contacts) + { + var emails = new List(); + if (c.Emails is not null) + { + foreach (var e in c.Emails) + { + if (!string.IsNullOrEmpty(e.EmailAddress)) + emails.Add(e.EmailAddress); + } + } + result.Add(new ContactDto(c.Id, c.DisplayName ?? string.Empty, emails)); + } + return result; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"ContactService: {ex.Message}"); + return Array.Empty(); + } + } +} +#endif diff --git a/src/PostIt/PostIt/Services/ContactService.cs b/src/PostIt/PostIt/Services/ContactService.cs deleted file mode 100644 index bc71d11b..00000000 --- a/src/PostIt/PostIt/Services/ContactService.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Maui.ApplicationModel.Communication; -using Microsoft.Maui.ApplicationModel; -using Microsoft.Maui.Devices; - -public class ContactService -{ - public async Task> GetDeviceContactsAsync() - { - // 1. Ensure the platform supports MAUI Essentials APIs - if (DeviceInfo.Current.Platform == DevicePlatform.Unknown) - { - throw new PlatformNotSupportedException("MAUI Essentials is not available on this platform."); - } - - try - { - // 2. Request runtime permission (Required for Android & iOS) - var status = await Permissions.RequestAsync(); - if (status != PermissionStatus.Granted) - { - // Permission denied by user - return Array.Empty(); - } - - // 3. Fetch all contacts - var contactsEnumerable = await Contacts.Default.GetAllAsync(); - return contactsEnumerable ?? Array.Empty(); - } - catch (Exception ex) - { - // Handle cross-platform exceptions or logs here - System.Diagnostics.Debug.WriteLine($"Error fetching contacts: {ex.Message}"); - return Array.Empty(); - } - } -} diff --git a/src/PostIt/PostIt/Services/IContactService.cs b/src/PostIt/PostIt/Services/IContactService.cs new file mode 100644 index 00000000..8c6da44a --- /dev/null +++ b/src/PostIt/PostIt/Services/IContactService.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PostIt.Services; + +/// +/// Abstraction over device contact providers (MAUI Essentials on mobile, +/// future Google/Exchange/IMAP providers). +/// +/// Implementations live next to this file in platform-conditional +/// source files: ContactService.Mobile.cs (ANDROID/IOS) and +/// ContactService.Desktop.cs (everything else). +/// +public interface IContactService +{ + Task> GetDeviceContactsAsync(CancellationToken ct = default); +} + +/// +/// Platform-neutral contact DTO. Source-of-truth shape for the UI layer; +/// concrete providers (MAUI Essentials today, Google Contacts API later) +/// map to this type. +/// +public sealed record ContactDto( + string Id, + string DisplayName, + IReadOnlyList Emails);