diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
index 83f325dc2e..2e6ef10c9f 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
@@ -349,7 +349,7 @@
"path": "README.md"
},
{
- "path": "Boilerplate.sln"
+ "path": "Boilerplate.Web.slnf"
}
],
"sources": [
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj
index 20e9fe9af1..efdc9e3e6a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj
@@ -77,7 +77,7 @@
-
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs
index b3fd90bae4..cdf63beadb 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs
@@ -42,7 +42,7 @@ public partial class AppComponentBase : ComponentBase, IAsyncDisposable
private readonly CancellationTokenSource cts = new();
protected CancellationToken CurrentCancellationToken => cts.Token;
- protected bool InPrerenderSession => JSRuntime.IsInitialized() is false;
+ protected bool InPrerenderSession => AppPlatform.IsBlazorHybrid is false && JSRuntime.IsInitialized() is false;
protected sealed override void OnInitialized()
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs
index e4e3efa550..6dff25ac30 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs
@@ -21,6 +21,7 @@ public partial class AppInitializer : AppComponentBase
[AutoInject] private IStorageService storageService = default!;
[AutoInject] private CultureInfoManager cultureInfoManager = default!;
[AutoInject] private NavigationManager navigationManager = default!;
+ [AutoInject] private Bit.Butil.Console console = default!;
protected async override Task OnInitAsync()
{
@@ -38,6 +39,8 @@ await storageService.GetItem("Culture") ?? // 2- User settings
}
await SetupBodyClasses();
+
+ BrowserConsoleLoggerProvider.SetConsole(console);
}
await base.OnInitAsync();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs
index 3ebd5e5836..4e0e93df6a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs
@@ -97,7 +97,7 @@ private async Task DeleteCategory()
try
{
- await categoryController.Delete(deletingCategory.Id, CurrentCancellationToken);
+ await categoryController.Delete(deletingCategory.Id, deletingCategory.ConcurrencyStamp.ToStampString(), CurrentCancellationToken);
await RefreshData();
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs
index 966f0e357e..055df432d0 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs
@@ -119,7 +119,7 @@ private async Task DeleteProduct()
try
{
- await productController.Delete(deletingProduct.Id, CurrentCancellationToken);
+ await productController.Delete(deletingProduct.Id, deletingProduct.ConcurrencyStamp.ToStampString(), CurrentCancellationToken);
await RefreshData();
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs
index c3a1415336..9d76ecf3ec 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs
@@ -1,4 +1,6 @@
-namespace Boilerplate.Client.Core.Components.Pages;
+using Microsoft.Extensions.Logging;
+
+namespace Boilerplate.Client.Core.Components.Pages;
public partial class NotAuthorizedPage
{
@@ -6,6 +8,8 @@ public partial class NotAuthorizedPage
[SupplyParameterFromQuery(Name = "return-url"), Parameter] public string? ReturnUrl { get; set; }
+ [AutoInject] private ILogger logger = default!;
+
protected override async Task OnParamsSetAsync()
{
user = (await AuthenticationStateTask).User;
@@ -25,6 +29,8 @@ protected override async Task OnAfterFirstRenderAsync()
{
await AuthenticationManager.RefreshToken();
+ logger.LogInformation("Refreshing access token.");
+
if ((await AuthenticationStateTask).User.IsAuthenticated())
{
if (ReturnUrl is not null)
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs
index e0c1c2c508..9cdf639f43 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs
@@ -15,7 +15,9 @@ await Task.Run(async () =>
}
});
- if (CultureInfoManager.MultilingualEnabled && forceLoad == false)
+ if (CultureInfoManager.MultilingualEnabled &&
+ forceLoad == false &&
+ (AppPlatform.IsAndroid || AppPlatform.IsIOS))
{
var currentCulture = CultureInfo.CurrentUICulture.Name;
var uri = new Uri(url);
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ByteArrayExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ByteArrayExtensions.cs
new file mode 100644
index 0000000000..d84ee60dd2
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ByteArrayExtensions.cs
@@ -0,0 +1,14 @@
+namespace System;
+
+public static class ByteArrayExtensions
+{
+ public static string ToStampString(this byte[]? source)
+ {
+ if (source is null or { Length: 0 })
+ source = [0, 0, 0, 0, 0, 0, 0, 0];
+
+ var base64String = Convert.ToBase64String(source);
+
+ return Uri.EscapeDataString(base64String);
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs
similarity index 99%
rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IServiceCollectionExtensions.cs
rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs
index 406c441915..81df0f17e2 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IServiceCollectionExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs
@@ -14,7 +14,7 @@
namespace Microsoft.Extensions.DependencyInjection;
-public static partial class IServiceCollectionExtensions
+public static partial class IClientCoreServiceCollectionExtensions
{
public static IServiceCollection AddClientCoreProjectServices(this IServiceCollection services, IConfiguration configuration)
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs
index 0f74c8543c..48518d9a2f 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs
@@ -1,4 +1,5 @@
//+:cnd:noEmit
+using System.Reflection;
//#if (notification == true)
using Boilerplate.Shared.Dtos.PushNotification;
//#endif
@@ -47,7 +48,8 @@ public static bool IsInitialized(this IJSRuntime jsRuntime)
{
"UnsupportedJavaScriptRuntime" => false, // pre-rendering
"RemoteJSRuntime" /* blazor server */ => (bool)type.GetProperty("IsInitialized")!.GetValue(jsRuntime)!,
- _ => true // blazor wasm / hybrid
+ "WebViewJSRuntime" /* blazor hybrid */ => type.GetField("_ipcSender", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(jsRuntime) is not null,
+ _ => true // blazor wasm
};
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs
new file mode 100644
index 0000000000..b3088e8582
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs
@@ -0,0 +1,11 @@
+namespace Microsoft.Extensions.Logging;
+
+public static class ILoggingBuilderExtensions
+{
+ public static ILoggingBuilder AddBrowserConsoleLogger(this ILoggingBuilder builder)
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+
+ return builder;
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs
index 9bab3b3884..4368df6e3a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs
@@ -13,7 +13,7 @@ public static partial class AppPlatform
public static bool IsAndroid => OperatingSystem.IsAndroid();
[SupportedOSPlatformGuard("ios")]
- public static bool IsIOS => OperatingSystem.IsIOS();
+ public static bool IsIOS => OperatingSystem.IsIOS() && !IsIosOnMacOS;
[SupportedOSPlatformGuard("windows")]
public static bool IsWindows => OperatingSystem.IsWindows();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs
index 6a94645c5b..daf233a748 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs
@@ -77,7 +77,9 @@ public override async Task GetAuthenticationStateAsync()
{
var access_token = await prerenderStateService.GetValue(() => tokenProvider.GetAccessToken());
- if (string.IsNullOrEmpty(access_token) && jsRuntime.IsInitialized())
+ bool inPrerenderSession = AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized() is false;
+
+ if (string.IsNullOrEmpty(access_token) && inPrerenderSession is false)
{
string? refresh_token = await storageService.GetItem("refresh_token");
@@ -126,7 +128,7 @@ private async Task StoreTokens(TokenResponseDto response, bool? rememberMe = nul
await storageService.SetItem("access_token", response!.AccessToken, rememberMe is true);
await storageService.SetItem("refresh_token", response!.RefreshToken, rememberMe is true);
- if (jsRuntime.IsInitialized())
+ if (AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized())
{
await cookie.Set(new()
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/BrowserConsoleLoggerProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/BrowserConsoleLoggerProvider.cs
new file mode 100644
index 0000000000..6cb91bca63
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/BrowserConsoleLoggerProvider.cs
@@ -0,0 +1,90 @@
+//+:cnd:noEmit
+using Microsoft.Extensions.Logging;
+
+namespace Boilerplate.Client.Core.Services;
+
+// https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/developer-tools
+
+///
+/// This logger writes to the browser console in blazor hybrid.
+///
+[ProviderAlias("BrowserConsolelogger")]
+public partial class BrowserConsoleLoggerProvider : ILoggerProvider, ILogger, IDisposable
+{
+ private static Bit.Butil.Console? console;
+
+ public static void SetConsole(Bit.Butil.Console console)
+ {
+ if (AppPlatform.IsBlazorHybrid is false)
+ throw new InvalidOperationException();
+ BrowserConsoleLoggerProvider.console = console;
+ }
+
+ public string? CategoryName { get; init; }
+
+ private static readonly ConcurrentQueue