Skip to content

Commit

Permalink
feat(templates): add web push feature to Boilerplate #8834 (#8835)
Browse files Browse the repository at this point in the history
  • Loading branch information
ysmoradi authored Oct 11, 2024
1 parent 27dc650 commit 05dc092
Show file tree
Hide file tree
Showing 65 changed files with 528 additions and 366 deletions.
1 change: 1 addition & 0 deletions .github/workflows/admin-sample.cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
env:
ServerAddress: ${{ env.API_SERVER_ADDRESS }}
ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }}
AdsPush.Primary.Vapid.PublicKey: ${{ secrets.ADMINPANEL_PUBLIC_VAPIDKEY }}

- uses: actions/setup-node@v4
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/todo-sample.cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
env:
ServerAddress: ${{ env.SERVER_ADDRESS }}
ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }}
AdsPush.Primary.Vapid.PublicKey: ${{ secrets.TODO_PUBLIC_VAPIDKEY }}

- name: Install wasm
run: cd src && dotnet workload install wasm-tools
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
files: 'src/Client/Boilerplate.Client.Core/appsettings.json, src/Shared/appsettings.json'
env:
ServerAddress: ${{ env.SERVER_ADDRESS }}
#if (notifications == true)
AdsPush.Primary.Vapid.PublicKey: ${{ secrets.PUBLIC_VAPIDKEY }}
#endif

- name: Install wasm
run: cd src && dotnet workload install wasm-tools
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,11 @@
"description": "Add SignalR sample."
},
"notification": {
"displayName": "Add Azure Notifications Hub?",
"displayName": "Add push notification?",
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"description": "Add Azure Notifications Hub."
"description": "Add push notification."
},
"offlineDb": {
"displayName": "Add Offline db?",
Expand Down Expand Up @@ -427,11 +427,12 @@
"src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs",
"src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs",
"src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs",
"src/Server/Boilerplate.Server.Api/Controllers/NotificationHubController.cs",
"src/Server/Boilerplate.Server.Api/Models/PushNotification/**",
"src/Server/Boilerplate.Server.Api/Services/AzureNotificationHubService.cs",
"src/Shared/Controllers/INotificationHubController.cs",
"src/Shared/Dtos/PushNotification/DeviceInstallationDto.cs"
"src/Server/Boilerplate.Server.Api/Models/PushNotification/**",
"src/Server/Boilerplate.Server.Api/Controllers/PushNotification/**",
"src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs",
"src/Shared/Controllers/PushNotification/**",
"src/Shared/Dtos/PushNotification/**",
"src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs"
]
},
{
Expand Down
1 change: 1 addition & 0 deletions src/Templates/Boilerplate/Bit.Boilerplate/Boilerplate.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".SolutionItems", ".Solution
.editorconfig = .editorconfig
.gitignore = .gitignore
.vsconfig = .vsconfig
settings.VisualStudio.json = settings.VisualStudio.json
Clean.bat = Clean.bat
src\Directory.Build.props = src\Directory.Build.props
src\Directory.Packages.props = src\Directory.Packages.props
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* Visual Studio Settings File */
{
"debugging.general.disableJITOptimization": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<Exec Command="npm install" StandardOutputImportance="high" StandardErrorImportance="high" />
</Target>

<Target Name="BuildJavaScript" Inputs="@(TypeScriptFiles)" Outputs="wwwroot\scripts\app.js">
<Target Name="BuildJavaScript" Inputs="@(TypeScriptFiles);tsconfig.json;package.json" Outputs="wwwroot\scripts\app.js">
<Exec Command="node_modules/.bin/tsc" StandardOutputImportance="high" StandardErrorImportance="high" />
<Exec Condition=" '$(Configuration)' == 'Release' " Command="node_modules/.bin/esbuild wwwroot/scripts/app.js --minify --outfile=wwwroot/scripts/app.js --allow-overwrite" StandardOutputImportance="high" StandardErrorImportance="high" />
</Target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private async void AuthenticationStateChanged(Task<AuthenticationState> task)
//#if (notification == true)
if (InPrerenderSession is false)
{
await pushNotificationService.RegisterDeviceAsync(CurrentCancellationToken);
await pushNotificationService.RegisterDevice(CurrentCancellationToken);
}
//#endif
}
Expand All @@ -94,7 +94,7 @@ private async Task ConnectSignalR()
await hubConnection.DisposeAsync();
}

var access_token = await AuthTokenProvider.GetAccessTokenAsync();
var access_token = await AuthTokenProvider.GetAccessToken();

hubConnection = new HubConnectionBuilder()
.WithUrl($"{Configuration.GetServerAddress()}/app-hub?access_token={access_token}", options =>
Expand All @@ -107,9 +107,9 @@ private async Task ConnectSignalR()
})
.Build();

hubConnection.On<string>("TwoFactorToken", async (token) =>
hubConnection.On<string>("DisplayMessage", async (message) =>
{
await messageBoxService.Show(Localizer[nameof(AppStrings.TwoFactorTokenPushText), token]);
await messageBoxService.Show(message);

// The following code block is not required for Bit.BlazorUI components to perform UI changes. However, it may be necessary in other scenarios.
/*await InvokeAsync(async () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected override async Task OnInitAsync()
user = (await PrerenderStateService.GetValue(() => HttpClient.GetFromJsonAsync("api/User/GetCurrentUser", JsonSerializerOptions.GetTypeInfo<UserDto>(), CurrentCancellationToken)))!;

var serverAddress = Configuration.GetServerAddress();
var access_token = await PrerenderStateService.GetValue(() => AuthTokenProvider.GetAccessTokenAsync());
var access_token = await PrerenderStateService.GetValue(() => AuthTokenProvider.GetAccessToken());
profileImageUrl = $"{serverAddress}/api/Attachment/GetProfileImage?access_token={access_token}";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override async Task OnInitAsync()
user = (await PrerenderStateService.GetValue(() => HttpClient.GetFromJsonAsync("api/User/GetCurrentUser", JsonSerializerOptions.GetTypeInfo<UserDto>(), CurrentCancellationToken)))!;

var serverAddress = Configuration.GetServerAddress();
var access_token = await PrerenderStateService.GetValue(() => AuthTokenProvider.GetAccessTokenAsync());
var access_token = await PrerenderStateService.GetValue(() => AuthTokenProvider.GetAccessToken());
profileImageUrl = $"{serverAddress}/api/Attachment/GetProfileImage?access_token={access_token}";

await base.OnInitAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public partial class AddOrEditProductModal

protected override async Task OnInitAsync()
{
await LoadAllCategoriesAsync();
await LoadAllCategories();
}

public async Task ShowModal(ProductDto productToShow)
Expand All @@ -35,7 +35,7 @@ await InvokeAsync(() =>
});
}

private async Task LoadAllCategoriesAsync()
private async Task LoadAllCategories()
{
isLoading = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public UserDto User

protected override async Task OnInitAsync()
{
var access_token = await PrerenderStateService.GetValue(AuthTokenProvider.GetAccessTokenAsync);
var access_token = await PrerenderStateService.GetValue(AuthTokenProvider.GetAccessToken);

removeProfileImageHttpUrl = $"api/Attachment/RemoveProfileImage?access_token={access_token}";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//+:cnd:noEmit
using System.Reflection;
//#if (notification == true)
using Boilerplate.Shared.Dtos.PushNotification;
//#endif

namespace Microsoft.JSInterop;

Expand Down Expand Up @@ -27,6 +29,13 @@ public static ValueTask<string> GoogleRecaptchaReset(this IJSRuntime jsRuntime)
}
//#endif

//#if (notification == true)
public static async ValueTask<DeviceInstallationDto> GetDeviceInstallation(this IJSRuntime jsRuntime, string vapidPublicKey)
{
return await jsRuntime.InvokeAsync<DeviceInstallationDto>("App.getDeviceInstallation", vapidPublicKey);
}
//#endif

/// <summary>
/// The return value would be false during pre-rendering
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class App {
//+:cnd:noEmit
class App {
public static applyBodyElementClasses(cssClasses: string[], cssVariables: any): void {
cssClasses?.forEach(c => document.body.classList.add(c));
Object.keys(cssVariables).forEach(key => document.body.style.setProperty(key, cssVariables[key]));
Expand All @@ -7,6 +8,27 @@
public static getPlatform(): string {
return (navigator as any).userAgentData?.platform || navigator?.platform;
}

//#if (notification == true)
public static async getDeviceInstallation(vapidPublicKey: string) {
if (await Notification.requestPermission() != "granted")
return null;
const registration = await navigator.serviceWorker.ready;
if (!registration) return null;
const pushManager = registration.pushManager;
let subscription = await pushManager.getSubscription();
if (subscription == null) {
subscription = await pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey
});
}
const pushChannel = subscription.toJSON();
const p256dh = pushChannel.keys!["p256dh"];
const auth = pushChannel.keys!["auth"];
return { installationId: `${p256dh}-${auth}`, platform: "browser", p256dh: p256dh, auth: auth, endpoint: pushChannel.endpoint };
};
//#endif
}

declare class BitTheme { static init(options: any): void; };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var access_token = await prerenderStateService.GetValue(() => tokenProvider.GetAccessTokenAsync());
var access_token = await prerenderStateService.GetValue(() => tokenProvider.GetAccessToken());

if (string.IsNullOrEmpty(access_token) && jsRuntime.IsInitialized())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public partial class ClientSideAuthTokenProvider : IAuthTokenProvider
{
[AutoInject] private IStorageService storageService = default!;

public async Task<string?> GetAccessTokenAsync()
public async Task<string?> GetAccessToken()
{
return await storageService.GetItem("access_token");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public interface IAuthTokenProvider
{
Task<string?> GetAccessTokenAsync();
Task<string?> GetAccessToken();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ public interface IPushNotificationService
{
string Token { get; set; }
bool NotificationsSupported { get; }
string GetDeviceId();
DeviceInstallationDto GetDeviceInstallation();
Task DeregisterDeviceAsync(CancellationToken cancellationToken);
Task RegisterDeviceAsync(CancellationToken cancellationToken);
Task<DeviceInstallationDto> GetDeviceInstallation();
Task RegisterDevice(CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
{
if (request.Headers.Authorization is null)
{
var access_token = await tokenProvider.GetAccessTokenAsync();
var access_token = await tokenProvider.GetAccessToken();
if (access_token is not null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
Expand Down Expand Up @@ -43,7 +43,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
// In the AuthenticationStateProvider, the access_token is refreshed using the refresh_token (if available).
await authManager.RefreshToken();

var access_token = await tokenProvider.GetAccessTokenAsync();
var access_token = await tokenProvider.GetAccessToken();

if (string.IsNullOrEmpty(access_token)) throw;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
using Boilerplate.Shared.Controllers;
using Boilerplate.Shared.Controllers.PushNotification;
using Boilerplate.Shared.Dtos.PushNotification;

namespace Boilerplate.Client.Core.Services;

public abstract partial class PushNotificationServiceBase : IPushNotificationService
{
[AutoInject] protected INotificationHubController pushNotificationController = default!;
[AutoInject] protected IStorageService storageService = default!;
[AutoInject] protected IPushNotificationController pushNotificationController = default!;
[AutoInject] protected IConfiguration configuration = default!;
[AutoInject] protected IJSRuntime jsRuntime = default!;
[AutoInject] protected JsonSerializerOptions jsonSerializerOptions = default!;

public virtual string Token { get; set; }
public virtual bool NotificationsSupported => false;
public virtual string GetDeviceId() => throw new NotImplementedException();
public virtual DeviceInstallationDto GetDeviceInstallation() => throw new NotImplementedException();

public async Task RegisterDeviceAsync(CancellationToken cancellationToken)
public virtual async Task<DeviceInstallationDto> GetDeviceInstallation()
{
if (NotificationsSupported is false)
return;

var deviceInstallation = GetDeviceInstallation();

await pushNotificationController.CreateOrUpdateInstallation(deviceInstallation, cancellationToken);

await storageService.SetItem("device_token", deviceInstallation.PushChannel);
return await jsRuntime.GetDeviceInstallation(configuration.GetRequiredValue<string>("AdsPush:Primary:Vapid:PublicKey"));
}

public async Task DeregisterDeviceAsync(CancellationToken cancellationToken)
public async Task RegisterDevice(CancellationToken cancellationToken)
{
if (NotificationsSupported is false)
return;

var cachedToken = await storageService.GetItem("device_token");
var deviceInstallation = await GetDeviceInstallation();

if (cachedToken == null)
if (deviceInstallation is null)
return;

var deviceId = GetDeviceId() ?? throw new InvalidOperationException("Unable to resolve an ID for the device.");

await pushNotificationController.DeleteInstallation(deviceId, cancellationToken);
await pushNotificationController.RegisterDevice(deviceInstallation, cancellationToken);
}

await storageService.RemoveItem("device_token");
public async Task DeregisterDevice(string deviceInstallationId, CancellationToken cancellationToken)
{
await pushNotificationController.DeregisterDevice(deviceInstallationId, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,16 @@
//#if (captcha == "reCaptcha")
"GoogleRecaptchaSiteKey": "6LdMKr4pAAAAAKMyuEPn3IHNf04EtULXA8uTIVRw",
//#endif
//#if (notification == true)
"AdsPush": {
"AdsPush_Comment": "https://github.com/adessoTurkey-dotNET/AdsPush",
"Primary": {
"Vapid": {
"Vapid_Comment": "Web push's vapid. More info at https://vapidkeys.com/",
"PublicKey": "BD0RrOIMGweHUdcs3H-YudqSxoRv06-ZSZc2jkD6nc9lhA5NONebNpvD570bY1T7WiMwGn-wlPGlEuGU8Bqt-oI"
}
}
},
//#endif
"$schema": "https://json.schemastore.org/appsettings.json"
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,25 @@ private static void SetupBlazorWebView()
#if Windows
webView.DefaultBackgroundColor = Color.FromArgb(webViewBackgroundColor).ToWindowsColor();

if (AppEnvironment.IsDev() is false)
{
webView.EnsureCoreWebView2Async()
.AsTask()
.ContinueWith(async _ =>
webView.EnsureCoreWebView2Async()
.AsTask()
.ContinueWith(async _ =>
{
await Application.Current!.Dispatcher.DispatchAsync(() =>
{
await Application.Current!.Dispatcher.DispatchAsync(() =>
webView.CoreWebView2.PermissionRequested += async (sender, args) =>
{
args.Handled = true;
args.State = Microsoft.Web.WebView2.Core.CoreWebView2PermissionState.Allow;
};
if (AppEnvironment.IsDev() is false)
{
var settings = webView.CoreWebView2.Settings;
settings.IsZoomControlEnabled = false;
settings.AreBrowserAcceleratorKeysEnabled = false;
});
}
});
}
});

#elif iOS || Mac
webView.NavigationDelegate = new CustomWKNavigationDelegate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Java.Net;
//#if (notification == true)
using Android.Gms.Tasks;
using Java.Net;
using Firebase.Messaging;
//#endif
using Boilerplate.Client.Core;
Expand Down
Loading

0 comments on commit 05dc092

Please sign in to comment.