diff --git a/src/NewsReader/NewsReader.Applications/Controllers/DataController.cs b/src/NewsReader/NewsReader.Applications/Controllers/DataController.cs index ecfbbaa1..f4368b34 100644 --- a/src/NewsReader/NewsReader.Applications/Controllers/DataController.cs +++ b/src/NewsReader/NewsReader.Applications/Controllers/DataController.cs @@ -1,5 +1,4 @@ -using Microsoft.AppCenter.Crashes; -using System.Waf.Applications; +using System.Waf.Applications; using System.Waf.Applications.Services; using System.Windows.Input; using Waf.NewsReader.Applications.Properties; diff --git a/src/NewsReader/NewsReader.Applications/Services/IWebStorageService.cs b/src/NewsReader/NewsReader.Applications/Services/IWebStorageService.cs index 61f2e748..0a1b8e6d 100644 --- a/src/NewsReader/NewsReader.Applications/Services/IWebStorageService.cs +++ b/src/NewsReader/NewsReader.Applications/Services/IWebStorageService.cs @@ -15,4 +15,4 @@ public interface IWebStorageService : INotifyPropertyChanged Task UploadFile(Stream source); } -public sealed record UserAccount(string UserName, string Email); +public sealed record UserAccount(string UserName, string? Email); diff --git a/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidManifest.xml b/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidManifest.xml index ab6d4bb8..3c93b284 100644 --- a/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidManifest.xml +++ b/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidManifest.xml @@ -1,7 +1,22 @@  + + + + + + + + - + + + + + + + + \ No newline at end of file diff --git a/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidModule.cs b/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidModule.cs index 07d27abd..172aa5c9 100644 --- a/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidModule.cs +++ b/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/AndroidModule.cs @@ -1,6 +1,4 @@ using Autofac; -using Waf.NewsReader.MauiSystem.Platforms.Android.Services; -using Waf.NewsReader.Presentation.Services; namespace Waf.NewsReader.MauiSystem.Platforms.Android; @@ -8,6 +6,5 @@ internal sealed class AndroidModule : Module { protected override void Load(ContainerBuilder builder) { - builder.RegisterType().As().SingleInstance(); } } diff --git a/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/Services/IdentityService.cs b/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/Services/IdentityService.cs deleted file mode 100644 index a1b916b4..00000000 --- a/src/NewsReader/NewsReader.MauiSystem/Platforms/Android/Services/IdentityService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Identity.Client; -using Waf.NewsReader.Presentation.Services; - -namespace Waf.NewsReader.MauiSystem.Platforms.Android.Services; - -public class IdentityService : IIdentityService -{ - public void Build(PublicClientApplicationBuilder builder) - { - builder.WithParentActivityOrWindow(() => Platform.CurrentActivity); - } -} \ No newline at end of file diff --git a/src/NewsReader/NewsReader.MauiSystem/Platforms/iOS/IosModule.cs b/src/NewsReader/NewsReader.MauiSystem/Platforms/iOS/IosModule.cs index 2292e4e3..90980124 100644 --- a/src/NewsReader/NewsReader.MauiSystem/Platforms/iOS/IosModule.cs +++ b/src/NewsReader/NewsReader.MauiSystem/Platforms/iOS/IosModule.cs @@ -1,7 +1,6 @@ using Autofac; using Waf.NewsReader.Applications.Services; using Waf.NewsReader.MauiSystem.Platforms.iOS.Services; -using Waf.NewsReader.Presentation.Services; namespace Waf.NewsReader.MauiSystem.Platforms.iOS; @@ -9,7 +8,6 @@ internal sealed class IosModule : Module { protected override void Load(ContainerBuilder builder) { - builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); } } diff --git a/src/NewsReader/NewsReader.MauiSystem/Platforms/iOS/Services/IdentityService.cs b/src/NewsReader/NewsReader.MauiSystem/Platforms/iOS/Services/IdentityService.cs deleted file mode 100644 index 389d1880..00000000 --- a/src/NewsReader/NewsReader.MauiSystem/Platforms/iOS/Services/IdentityService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Foundation; -using Microsoft.Identity.Client; -using Waf.NewsReader.Presentation.Services; - -namespace Waf.NewsReader.MauiSystem.Platforms.iOS.Services; - -public class IdentityService : IIdentityService -{ - public void Build(PublicClientApplicationBuilder builder) - { - builder.WithIosKeychainSecurityGroup(NSBundle.MainBundle.BundleIdentifier); - } -} \ No newline at end of file diff --git a/src/NewsReader/NewsReader.Presentation/NewsReader.Presentation.csproj b/src/NewsReader/NewsReader.Presentation/NewsReader.Presentation.csproj index 16bec8f6..b68f39df 100644 --- a/src/NewsReader/NewsReader.Presentation/NewsReader.Presentation.csproj +++ b/src/NewsReader/NewsReader.Presentation/NewsReader.Presentation.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/src/NewsReader/NewsReader.Presentation/Services/IIdentityService.cs b/src/NewsReader/NewsReader.Presentation/Services/IIdentityService.cs deleted file mode 100644 index b79d462f..00000000 --- a/src/NewsReader/NewsReader.Presentation/Services/IIdentityService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.Identity.Client; - -namespace Waf.NewsReader.Presentation.Services; - -public interface IIdentityService -{ - void Build(PublicClientApplicationBuilder builder); -} diff --git a/src/NewsReader/NewsReader.Presentation/Services/WebStorageService.cs b/src/NewsReader/NewsReader.Presentation/Services/WebStorageService.cs index 54a05a7c..0067a015 100644 --- a/src/NewsReader/NewsReader.Presentation/Services/WebStorageService.cs +++ b/src/NewsReader/NewsReader.Presentation/Services/WebStorageService.cs @@ -1,9 +1,6 @@ -using Microsoft.AppCenter.Crashes; -using Microsoft.Graph; +using Microsoft.Graph; using Microsoft.Identity.Client; -using System.Net; -using System.Net.Http.Headers; -using Waf.NewsReader.Applications; +using Microsoft.Kiota.Abstractions.Authentication; using Waf.NewsReader.Applications.Services; namespace Waf.NewsReader.Presentation.Services; @@ -26,7 +23,7 @@ internal sealed partial class WebStorageService : Model, IWebStorageService private GraphServiceClient? graphClient; private UserAccount? currentAccount; - public WebStorageService(IIdentityService? identityService = null) + public WebStorageService() { string? appId = null; GetApplicationId(ref appId); @@ -34,7 +31,11 @@ public WebStorageService(IIdentityService? identityService = null) { var builder = PublicClientApplicationBuilder.Create(appId); builder.WithRedirectUri("wafe5b8cee6-8ba0-46c5-96ef-a3c8a1e2bb26://auth"); - identityService?.Build(builder); +#if ANDROID + builder.WithParentActivityOrWindow(() => Platform.CurrentActivity); +#elif IOS + builder.WithIosKeychainSecurityGroup(Foundation.NSBundle.MainBundle.BundleIdentifier); +#endif publicClient = builder.Build(); } } @@ -58,8 +59,7 @@ public async Task TrySilentSignIn() } catch (Exception ex) { - Log.Default.TrackError(ex, "Silent login failed"); - // Ignore (e.g. no internet access) + Log.Default.TrackError(ex, "Silent login failed"); // Ignore (e.g. no internet access) } return false; } @@ -71,8 +71,7 @@ public async Task SignIn() { try { - var interactiveRequest = publicClient.AcquireTokenInteractive(scopes); - await interactiveRequest.ExecuteAsync(); + await publicClient.AcquireTokenInteractive(scopes).ExecuteAsync(); } catch (MsalClientException ex) when (ex.ErrorCode == MsalError.AuthenticationCanceledError) { @@ -95,7 +94,7 @@ public async Task SignOut() private async Task TrySilentSignInCore() { - if (publicClient == null) return null; + if (publicClient is null) return null; try { var accounts = await publicClient.GetAccountsAsync(); @@ -114,42 +113,60 @@ public async Task SignOut() private async Task InitGraphClient() { - graphClient = new GraphServiceClient(new DelegateAuthenticationProvider( - async requestMessage => - { - if (publicClient == null) return; - var accounts = await publicClient.GetAccountsAsync().ConfigureAwait(false); - if (!accounts.Any()) return; - var result = await publicClient.AcquireTokenSilent(scopes, accounts.First()).ExecuteAsync().ConfigureAwait(false); - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); - })); - var user = await graphClient.Me.Request().GetAsync(); - CurrentAccount = new(!string.IsNullOrEmpty(user.DisplayName) ? user.DisplayName : user.UserPrincipalName, user.Mail); + if (publicClient is null) return; + var authenticationProvider = new BaseBearerTokenAuthenticationProvider(new TokenProvider(publicClient)); + graphClient = new GraphServiceClient(authenticationProvider); + var user = await graphClient.Me.GetAsync(); + ArgumentNullException.ThrowIfNull(user); + CurrentAccount = new(!string.IsNullOrEmpty(user.DisplayName) ? user.DisplayName : user.UserPrincipalName ?? "", user.Mail); } public async Task<(Stream? stream, string? cTag)> DownloadFile(string? cTag) { if (graphClient == null) return default; - var item = graphClient.Me.Drive.Special.AppRoot.ItemWithPath(dataFileName); - try + var item = await GetItem(dataFileName).ConfigureAwait(false); + var metaItem = await item.GetAsync().ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(metaItem); + if (metaItem.CTag != cTag) { - var metaItem = await item.Request().GetAsync().ConfigureAwait(false); - if (metaItem.CTag != cTag) - { - return (await item.Content.Request().GetAsync().ConfigureAwait(false), metaItem.CTag); - } + return (await item.Content.GetAsync().ConfigureAwait(false), metaItem.CTag); } - catch (ServiceException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + // TODO: catch (ServiceException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } return default; } public async Task UploadFile(Stream source) { if (graphClient == null) return null; - var driveItem = await graphClient.Me.Drive.Special.AppRoot.ItemWithPath(dataFileName) - .Content.Request().PutAsync(source).ConfigureAwait(false); - return driveItem.CTag; + var item = await GetItem(dataFileName).ConfigureAwait(false); + var newItem = await item.Content.PutAsync(source).ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(newItem); + return newItem.CTag; + } + + private async Task GetItem(string fileName) + { + if (graphClient is null) throw new InvalidOperationException("graphClient is null"); + var driveItem = await graphClient.Me.Drive.GetAsync().ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(driveItem); + var appRootFolder = await graphClient.Drives[driveItem.Id].Special["AppRoot"].GetAsync().ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(appRootFolder); + return graphClient.Drives[driveItem.Id].Items[appRootFolder.Id].ItemWithPath(fileName); } static partial void GetApplicationId(ref string? applicationId); + + + private sealed class TokenProvider(IPublicClientApplication publicClient) : IAccessTokenProvider + { + public AllowedHostsValidator AllowedHostsValidator { get; } = new(); + + public async Task GetAuthorizationTokenAsync(Uri uri, Dictionary? additionalAuthenticationContext = null, CancellationToken cancellationToken = default) + { + var accounts = await publicClient.GetAccountsAsync().ConfigureAwait(false); + if (!accounts.Any()) return ""; + var result = await publicClient.AcquireTokenSilent(scopes, accounts.First()).ExecuteAsync(cancellationToken).ConfigureAwait(false); + return result.AccessToken; + } + } } diff --git a/src/NewsReader/NewsReader.Presentation/Views/ShellView.xaml.cs b/src/NewsReader/NewsReader.Presentation/Views/ShellView.xaml.cs index 8c3c41ac..3241614d 100644 --- a/src/NewsReader/NewsReader.Presentation/Views/ShellView.xaml.cs +++ b/src/NewsReader/NewsReader.Presentation/Views/ShellView.xaml.cs @@ -33,7 +33,7 @@ public async Task PushAsync(object page) // Pushing of a page which already exists in the navigation stack is not allowed -> InvalidOperationException: 'Page must not already have a parent.' // If the specified page already exists in the navigaton stack then remove all pages after the page and pop to it. if (idx == navi.NavigationStack.Count - 1) return; - for (int i = 0; i < navi.NavigationStack.Count - idx - 2; i++) navi.RemovePage(navi.NavigationStack[navi.NavigationStack.Count - 2]); + for (int i = 0; i < navi.NavigationStack.Count - idx - 2; i++) navi.RemovePage(navi.NavigationStack[^2]); await navi.PopAsync(); } else await navi.PushAsync((Page)page);