Skip to content

Commit

Permalink
Better support for scenarios where JS is unavailable
Browse files Browse the repository at this point in the history
  • Loading branch information
RyanTT committed Jul 7, 2022
1 parent cba6df4 commit 2423321
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<SignAssembly>False</SignAssembly>
<PackageProjectUrl>https://github.com/BytexDigital/BytexDigital.Blazor.Components.CookieConsent</PackageProjectUrl>
<PackageIcon>logo_squared_128.png</PackageIcon>
<PackageVersion>1.0.13</PackageVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
140 changes: 92 additions & 48 deletions BytexDigital.Blazor.Components.CookieConsent/CookieConsentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;

Expand All @@ -10,19 +11,33 @@ namespace BytexDigital.Blazor.Components.CookieConsent
public class CookieConsentService
{
private readonly IJSRuntime _jsRuntime;
private readonly ILogger<CookieConsentService> _logger;
private readonly IOptions<CookieConsentOptions> _options;

private Task<IJSObjectReference> _module;
private CookiePreferences _cookiePreferencesCached = default;

private Task<IJSObjectReference> Module => _module ??= _jsRuntime.InvokeAsync<IJSObjectReference>(
"import",
new[] { "./_content/BytexDigital.Blazor.Components.CookieConsent/cookieconsent.js" })
.AsTask();

public CookieConsentService(IOptions<CookieConsentOptions> options, IJSRuntime jsRuntime)
public CookieConsentService(
IOptions<CookieConsentOptions> options,
IJSRuntime jsRuntime,
ILogger<CookieConsentService> logger)
{
_options = options;
_jsRuntime = jsRuntime;
_logger = logger;

// Create a default cookie preferences object that is returned when Javascript turns out to be unavailable
// and no call to SavePreferences has been made yet.
_cookiePreferencesCached = new CookiePreferences
{
AcceptedRevision = -1,
AllowedCategories = new[] { CookieCategory.NecessaryCategoryIdentifier }
};
}

public event EventHandler<CookiePreferences> CookiePreferencesChanged;
Expand All @@ -31,10 +46,7 @@ public CookieConsentService(IOptions<CookieConsentOptions> options, IJSRuntime j

public async Task ShowConsentModalAsync(bool showOnlyIfNecessary)
{
if (showOnlyIfNecessary && await IsCurrentRevisionAcceptedAsync())
{
return;
}
if (showOnlyIfNecessary && await IsCurrentRevisionAcceptedAsync()) return;

await Task.Run(() => OnShowConsentModal?.Invoke(this, EventArgs.Empty));
}
Expand All @@ -46,22 +58,45 @@ public async Task ShowSettingsModalAsync()

public async Task SavePreferencesAsync(CookiePreferences cookiePreferences)
{
// Fetch the currently valid settings.
// If JS is unavailable, this will return default settings or the last written settings from memory cache.
var existingPreferences = await GetPreferencesAsync();

// Attempt to write the new settings object to our cookie if possible.
try
{
var module = await Module;

var module = await Module;

await module.InvokeVoidAsync(
"CookieConsent.SetCookie",
CreateCookieString(JsonSerializer.Serialize(cookiePreferences)));
await module.InvokeVoidAsync(
"CookieConsent.SetCookie",
CreateCookieString(JsonSerializer.Serialize(cookiePreferences)));

await module.InvokeVoidAsync(
"CookieConsent.ApplyPreferences",
cookiePreferences.AllowedCategories,
cookiePreferences.AllowedServices);
await module.InvokeVoidAsync(
"CookieConsent.ApplyPreferences",
cookiePreferences.AllowedCategories,
cookiePreferences.AllowedServices);
}
catch (JSException ex)
{
// Ignore exceptions on purpose.
// We might get here because JS is blocked or disabled.
_logger.LogTrace(ex, "Exception raised attempting to call into JavaScript");
}

// In either case, we always save the instance that is supposed to be valid in our memory cache.
// We don't want to "forget" written things even if JS was unavailable to actually permanently save them!
// This solution will at least allow us to remember written settings until our tab is closed.
_cookiePreferencesCached = cookiePreferences;

if (!existingPreferences.Equals(cookiePreferences))
try
{
await Task.Run(() => CookiePreferencesChanged?.Invoke(this, cookiePreferences));
if (!existingPreferences.Equals(cookiePreferences))
await Task.Run(() => CookiePreferencesChanged?.Invoke(this, cookiePreferences));
}
catch (Exception ex)
{
// Ignore most likely user caused exception and log it as we don't want to interrupt program flow.
_logger.LogError(ex, "Exception raised trying to run CookiePreferencesChanged event handler");
}
}

Expand Down Expand Up @@ -94,6 +129,12 @@ await SavePreferencesAsync(
});
}

/// <summary>
/// Returns the currently valid <see cref="CookiePreferences"/> instance. If JavaScript is unavailable, will return
/// either an instance with only the necessary category enabled or the last written settings object given to
/// <see cref="SavePreferencesAsync"/>.
/// </summary>
/// <returns></returns>
public async Task<CookiePreferences> GetPreferencesAsync()
{
try
Expand All @@ -105,14 +146,13 @@ public async Task<CookiePreferences> GetPreferencesAsync()

return JsonSerializer.Deserialize<CookiePreferences>(cookieValue);
}
catch (Exception)
catch (JSException ex)

This comment has been minimized.

Copy link
@bent-rasmussen

bent-rasmussen Jul 7, 2022

It seems like this should have caught a System.Text.Json.JsonException instead of a JSException.

{
// During prerendering, we will not be able to access JS interop. Thus we must assume we have no preferences set except the necessary ones.
return new CookiePreferences
{
AcceptedRevision = -1,
AllowedCategories = new[] { CookieCategory.NecessaryCategoryIdentifier }
};
// We might get here because JS is unavailable (blocked, prerendering, etc.).
// Return default/cached data instead.
_logger.LogTrace(ex, "Exception raised attempting to call into JavaScript");

return _cookiePreferencesCached;
}
}

Expand Down Expand Up @@ -159,18 +199,36 @@ public async Task<bool> IsCurrentRevisionAcceptedAsync()

public async Task NotifyPageLoadedAsync()
{
if (await IsCurrentRevisionAcceptedAsync())
var preferences = await GetPreferencesAsync();

try
{
var preferences = await GetPreferencesAsync();

await Task.Run(() => CookiePreferencesChanged?.Invoke(this, preferences));
if (await IsCurrentRevisionAcceptedAsync())
{
var module = await Module;

var module = await Module;
await module.InvokeVoidAsync(
"CookieConsent.ApplyPreferences",
preferences.AllowedCategories,
preferences.AllowedServices);
}
}
catch (JSException ex)
{
// Ignore exceptions due to blocked or unavailable JS.
// In this case, we aren't able to activate JS tags, but there seems to be nothing
// we can do in this situation!
_logger.LogTrace(ex, "Exception raised attempting to call into JavaScript");
}

await module.InvokeVoidAsync(
"CookieConsent.ApplyPreferences",
preferences.AllowedCategories,
preferences.AllowedServices);
try
{
await Task.Run(() => CookiePreferencesChanged?.Invoke(this, preferences));
}
catch (Exception ex)
{
// Ignore most likely user caused exception and log it as we don't want to interrupt program flow.
_logger.LogError(ex, "Exception raised trying to run CookiePreferencesChanged event handler");
}
}

Expand All @@ -180,34 +238,20 @@ protected virtual string CreateCookieString(string value)
cookieString += $"; samesite={_options.Value.CookieOptions.CookieSameSite}";

if (_options.Value.CookieOptions.CookieMaxAge != default)
{
cookieString += $"; max-age={(int) _options.Value.CookieOptions.CookieMaxAge.Value.TotalSeconds}";
}

if (!string.IsNullOrEmpty(_options.Value.CookieOptions.CookieDomain))
{
cookieString += $"; domain={_options.Value.CookieOptions.CookieDomain}";
}

if (!string.IsNullOrEmpty(_options.Value.CookieOptions.CookiePath))
{
cookieString += $"; path={_options.Value.CookieOptions.CookiePath}";
}

if (_options.Value.CookieOptions.CookieHttpOnly)
{
cookieString += "; HttpOnly";
}
if (_options.Value.CookieOptions.CookieHttpOnly) cookieString += "; HttpOnly";

if (_options.Value.CookieOptions.CookieSecure)
{
cookieString += "; Secure";
}
if (_options.Value.CookieOptions.CookieSecure) cookieString += "; Secure";

if (_options.Value.CookieOptions.CookieExpires != default)
{
cookieString += $"; expires={_options.Value.CookieOptions.CookieExpires.Value:r}";
}

return cookieString;
}
Expand Down

0 comments on commit 2423321

Please sign in to comment.