feat/postit-acl #32

Merged
notazof merged 33 commits from feat/postit-acl into release/1.0.7 2026-08-18 16:14:32 +01:00
4 changed files with 120 additions and 39 deletions
Showing only changes of commit 69a660cafb - Show all commits

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.
Paul Schneider 2026-08-17 23:13:35 +01:00
Signed by: notazof
GPG key ID: 1DD5D838E5343B06

View file

@ -0,0 +1,27 @@
#if !ANDROID && !IOS
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace PostIt.Services;
/// <summary>
/// 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.
/// </summary>
public sealed class ContactService : IContactService
{
public Task<IReadOnlyList<ContactDto>> GetDeviceContactsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ContactDto>>(Array.Empty<ContactDto>());
}
#endif

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
public sealed class ContactService : IContactService
{
public async Task<IReadOnlyList<ContactDto>> GetDeviceContactsAsync(CancellationToken ct = default)
{
if (DeviceInfo.Current.Platform == DevicePlatform.Unknown)
return Array.Empty<ContactDto>();
try
{
var status = await Permissions.RequestAsync<Permissions.ContactsRead>();
if (status != PermissionStatus.Granted)
return Array.Empty<ContactDto>();
var contacts = await Contacts.Default.GetAllAsync();
if (contacts is null) return Array.Empty<ContactDto>();
var result = new List<ContactDto>();
foreach (var c in contacts)
{
var emails = new List<string>();
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<ContactDto>();
}
}
}
#endif

View file

@ -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<IEnumerable<Contact>> 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<Permissions.ContactsRead>();
if (status != PermissionStatus.Granted)
{
// Permission denied by user
return Array.Empty<Contact>();
}
// 3. Fetch all contacts
var contactsEnumerable = await Contacts.Default.GetAllAsync();
return contactsEnumerable ?? Array.Empty<Contact>();
}
catch (Exception ex)
{
// Handle cross-platform exceptions or logs here
System.Diagnostics.Debug.WriteLine($"Error fetching contacts: {ex.Message}");
return Array.Empty<Contact>();
}
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace PostIt.Services;
/// <summary>
/// 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).
/// </summary>
public interface IContactService
{
Task<IReadOnlyList<ContactDto>> GetDeviceContactsAsync(CancellationToken ct = default);
}
/// <summary>
/// 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.
/// </summary>
public sealed record ContactDto(
string Id,
string DisplayName,
IReadOnlyList<string> Emails);