From f7dd1f2c150ac1e1d0401713a75b4f02347bb409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= <33655937+jkoenig134@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:04:35 +0200 Subject: [PATCH] Feature/device push identifier (#148) * feat: update backbone return type * feat: handle device push identifier in the app-runtime * fix: add devicePushIdentifier to LocalAccountDTO * chore: add lgos * chore: simplify the event bus and make it actually work * fix: assert on the dpi * feat: add a test for push notifications * fix: remove TODO comment * chore: bumpy * chore: bump transport --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- package-lock.json | 10 +-- packages/app-runtime/package.json | 4 +- .../IBackboneEventContent.ts | 2 +- .../PushNotificationModule.ts | 31 ++++++--- .../multiAccount/MultiAccountController.ts | 20 ++++++ .../src/multiAccount/data/LocalAccount.ts | 5 ++ .../src/multiAccount/data/LocalAccountDTO.ts | 1 + .../multiAccount/data/LocalAccountMapper.ts | 3 +- .../multiAccount/data/LocalAccountSession.ts | 1 + .../test/mocks/NativeEventBusMock.ts | 32 +-------- .../test/modules/PushNotification.test.ts | 67 +++++++++++++++++++ packages/runtime/package.json | 4 +- .../facades/transport/AccountFacade.ts | 3 +- .../account/RegisterPushNotificationToken.ts | 12 ++-- .../runtime/test/transport/account.test.ts | 5 +- packages/transport/package.json | 2 +- .../src/modules/accounts/AccountController.ts | 5 +- .../devices/backbone/DeviceAuthClient.ts | 8 ++- 18 files changed, 152 insertions(+), 63 deletions(-) create mode 100644 packages/app-runtime/test/modules/PushNotification.test.ts diff --git a/package-lock.json b/package-lock.json index b4993a40b..824c8961b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12358,7 +12358,7 @@ }, "packages/app-runtime": { "name": "@nmshd/app-runtime", - "version": "3.2.1", + "version": "3.3.0", "license": "MIT", "dependencies": { "@js-soft/docdb-access-loki": "^1.1.0", @@ -12372,7 +12372,7 @@ "@types/luxon": "^3.4.2" }, "peerDependencies": { - "@nmshd/runtime": "^4.4.1" + "@nmshd/runtime": "^4.11.0" } }, "packages/consumption": { @@ -12424,7 +12424,7 @@ }, "packages/runtime": { "name": "@nmshd/runtime", - "version": "4.10.6", + "version": "4.11.0", "license": "MIT", "dependencies": { "@js-soft/docdb-querytranslator": "^1.1.4", @@ -12434,7 +12434,7 @@ "@nmshd/consumption": "3.11.0", "@nmshd/content": "2.10.1", "@nmshd/crypto": "2.0.6", - "@nmshd/transport": "2.7.5", + "@nmshd/transport": "2.8.0", "ajv": "^8.16.0", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", @@ -12466,7 +12466,7 @@ }, "packages/transport": { "name": "@nmshd/transport", - "version": "2.7.5", + "version": "2.8.0", "license": "MIT", "dependencies": { "@js-soft/docdb-access-abstractions": "1.0.4", diff --git a/packages/app-runtime/package.json b/packages/app-runtime/package.json index 927740966..8ad1ff8c4 100644 --- a/packages/app-runtime/package.json +++ b/packages/app-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/app-runtime", - "version": "3.2.1", + "version": "3.3.0", "description": "The App Runtime", "homepage": "https://enmeshed.eu", "repository": { @@ -54,7 +54,7 @@ "@types/luxon": "^3.4.2" }, "peerDependencies": { - "@nmshd/runtime": "^4.4.1" + "@nmshd/runtime": "^4.11.0" }, "publishConfig": { "access": "public", diff --git a/packages/app-runtime/src/modules/pushNotifications/IBackboneEventContent.ts b/packages/app-runtime/src/modules/pushNotifications/IBackboneEventContent.ts index dafc64d10..fbf57dc1d 100644 --- a/packages/app-runtime/src/modules/pushNotifications/IBackboneEventContent.ts +++ b/packages/app-runtime/src/modules/pushNotifications/IBackboneEventContent.ts @@ -4,7 +4,7 @@ export enum BackboneEventName { } export interface IBackboneEventContent { - accRef: string; + devicePushIdentifier: string; eventName: BackboneEventName; sentAt: string; payload: any; diff --git a/packages/app-runtime/src/modules/pushNotifications/PushNotificationModule.ts b/packages/app-runtime/src/modules/pushNotifications/PushNotificationModule.ts index 09a0e1858..5c34a058f 100644 --- a/packages/app-runtime/src/modules/pushNotifications/PushNotificationModule.ts +++ b/packages/app-runtime/src/modules/pushNotifications/PushNotificationModule.ts @@ -23,8 +23,10 @@ export class PushNotificationModule extends AppRuntimeModule { + this.logger.trace("PushNotificationModule.registerPushIdentifierForAccount", { address, pushIdentifier: devicePushIdentifier }); + + await this.runtime.multiAccountController.updatePushIdentifierForAccount(address, devicePushIdentifier); } public getNotificationTokenFromConfig(): Result { diff --git a/packages/app-runtime/src/multiAccount/MultiAccountController.ts b/packages/app-runtime/src/multiAccount/MultiAccountController.ts index bd13a5f88..f4a9057f6 100644 --- a/packages/app-runtime/src/multiAccount/MultiAccountController.ts +++ b/packages/app-runtime/src/multiAccount/MultiAccountController.ts @@ -253,4 +253,24 @@ export class MultiAccountController { await this._localAccounts.update(document, localAccount); } + + public async updatePushIdentifierForAccount(address: string, devicePushIdentifier: string): Promise { + const document = await this._localAccounts.findOne({ address }); + if (!document) { + throw TransportCoreErrors.general.recordNotFound(LocalAccount, address).logWith(this._log); + } + + const localAccount = LocalAccount.from(document); + localAccount.devicePushIdentifier = devicePushIdentifier; + + await this._localAccounts.update(document, localAccount); + } + + public async getAccountReferenceForDevicePushIdentifier(devicePushIdentifier: string): Promise { + const document = await this._localAccounts.findOne({ devicePushIdentifier }); + if (!document) throw new Error(`Could not resolve a local account reference for the device push identifier '${devicePushIdentifier}'.`); + + const localAccount = LocalAccount.from(document); + return localAccount.id.toString(); + } } diff --git a/packages/app-runtime/src/multiAccount/data/LocalAccount.ts b/packages/app-runtime/src/multiAccount/data/LocalAccount.ts index 04ab38b72..3686f2571 100644 --- a/packages/app-runtime/src/multiAccount/data/LocalAccount.ts +++ b/packages/app-runtime/src/multiAccount/data/LocalAccount.ts @@ -9,6 +9,7 @@ export interface ILocalAccount extends ICoreSerializable { directory: string; order: number; lastAccessedAt?: ICoreDate; + devicePushIdentifier?: string; } @type("LocalAccount") @@ -41,6 +42,10 @@ export class LocalAccount extends CoreSerializable implements ILocalAccount { @serialize() public lastAccessedAt?: CoreDate; + @validate({ nullable: true }) + @serialize() + public devicePushIdentifier?: string; + public static from(value: ILocalAccount): LocalAccount { return this.fromAny(value); } diff --git a/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts b/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts index 7d1721d8a..afbcfc63c 100644 --- a/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts +++ b/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts @@ -6,4 +6,5 @@ export interface LocalAccountDTO { directory: string; order: number; lastAccessedAt?: string; + devicePushIdentifier?: string; } diff --git a/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts b/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts index d81d99d4f..e2c5cb537 100644 --- a/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts +++ b/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts @@ -10,7 +10,8 @@ export class LocalAccountMapper { realm: localAccount.realm, directory: localAccount.directory.toString(), order: localAccount.order, - lastAccessedAt: localAccount.lastAccessedAt?.toString() + lastAccessedAt: localAccount.lastAccessedAt?.toString(), + devicePushIdentifier: localAccount.devicePushIdentifier }; } } diff --git a/packages/app-runtime/src/multiAccount/data/LocalAccountSession.ts b/packages/app-runtime/src/multiAccount/data/LocalAccountSession.ts index c8a0f9c06..dfc62c117 100644 --- a/packages/app-runtime/src/multiAccount/data/LocalAccountSession.ts +++ b/packages/app-runtime/src/multiAccount/data/LocalAccountSession.ts @@ -15,4 +15,5 @@ export interface LocalAccountSession { consumptionController: ConsumptionController; selectedRelationship?: IdentityDVO; expiresAt?: string; + devicePushIdentifier?: string; } diff --git a/packages/app-runtime/test/mocks/NativeEventBusMock.ts b/packages/app-runtime/test/mocks/NativeEventBusMock.ts index eaeafa5da..202edd03a 100644 --- a/packages/app-runtime/test/mocks/NativeEventBusMock.ts +++ b/packages/app-runtime/test/mocks/NativeEventBusMock.ts @@ -1,14 +1,8 @@ -import { AppReadyEvent, INativeEventBus } from "@js-soft/native-abstractions"; +import { INativeEventBus } from "@js-soft/native-abstractions"; import { Event, EventBus, EventEmitter2EventBus, Result } from "@js-soft/ts-utils"; export class NativeEventBusMock implements INativeEventBus { private eventBus: EventBus; - private locked = true; - private queue: Event[] = []; - - public get isLocked(): boolean { - return this.locked; - } public init(): Promise> { this.eventBus = new EventEmitter2EventBus(() => { @@ -32,31 +26,7 @@ export class NativeEventBusMock implements INativeEventBus { return Result.ok(undefined); } - /** - * Publish Events on the EventBus. - * The EventBus is initially locked. - * Published events are queued to be published after the EventBus is unlocked. - * To unlock the EventBus an AppReadyEvent has to be published. - * @param event - * @returns - */ public publish(event: Event): Result { - if (this.locked) { - if (event instanceof AppReadyEvent) { - this.locked = false; - // eslint-disable-next-line no-console - console.log("Unlocked EventBus."); // No js-soft logger available at this stage - this.queue.forEach((event: Event) => this.publish(event)); - this.queue = []; - // eslint-disable-next-line no-console - console.log("All queued events have been published."); // No js-soft logger available at this stage - } else { - this.queue.push(event); - // eslint-disable-next-line no-console - console.warn("EventBus is locked. Queued the following event:", event); // No js-soft logger available at this stage - } - return Result.ok(undefined); - } this.eventBus.publish(event); return Result.ok(undefined); } diff --git a/packages/app-runtime/test/modules/PushNotification.test.ts b/packages/app-runtime/test/modules/PushNotification.test.ts new file mode 100644 index 000000000..a86b48d74 --- /dev/null +++ b/packages/app-runtime/test/modules/PushNotification.test.ts @@ -0,0 +1,67 @@ +import { RemoteNotificationEvent, RemoteNotificationRegistrationEvent } from "@js-soft/native-abstractions"; +import { sleep } from "@js-soft/ts-utils"; +import { AppRuntime, DatawalletSynchronizedEvent, ExternalEventReceivedEvent, LocalAccountSession } from "../../src"; +import { TestUtil } from "../lib"; + +describe("PushNotificationModuleTest", function () { + let runtime: AppRuntime; + let session: LocalAccountSession; + let devicePushIdentifier = "dummy value"; + + beforeAll(async function () { + runtime = await TestUtil.createRuntime(); + await runtime.start(); + + const accounts = await TestUtil.provideAccounts(runtime, 1); + session = await runtime.selectAccount(accounts[0].id); + }); + + afterAll(async function () { + await runtime.stop(); + }); + + test("should persist push identifier", async function () { + runtime.nativeEnvironment.eventBus.publish(new RemoteNotificationRegistrationEvent("handleLongerThan10Characters")); + + // wait for the registration to finish + // there is no event to wait for, so we just wait for a second + await sleep(1000); + + const account = await runtime.accountServices.getAccount(session.account.id); + expect(account.devicePushIdentifier).toBeDefined(); + + devicePushIdentifier = account.devicePushIdentifier!; + }); + + test("should do a datawallet sync when DatawalletModificationsCreated is received", async function () { + runtime.nativeEnvironment.eventBus.publish( + new RemoteNotificationEvent({ + content: { + devicePushIdentifier: devicePushIdentifier, + eventName: "DatawalletModificationsCreated", + sentAt: new Date().toISOString(), + payload: {} + } + }) + ); + + const event = await TestUtil.awaitEvent(runtime, DatawalletSynchronizedEvent); + expect(event).toBeDefined(); + }); + + test("should do a sync everything when ExternalEventCreated is received", async function () { + runtime.nativeEnvironment.eventBus.publish( + new RemoteNotificationEvent({ + content: { + devicePushIdentifier: devicePushIdentifier, + eventName: "ExternalEventCreated", + sentAt: new Date().toISOString(), + payload: {} + } + }) + ); + + const event = await TestUtil.awaitEvent(runtime, ExternalEventReceivedEvent); + expect(event).toBeDefined(); + }); +}); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 980446624..708811560 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/runtime", - "version": "4.10.6", + "version": "4.11.0", "description": "The enmeshed client runtime.", "homepage": "https://enmeshed.eu", "repository": { @@ -59,7 +59,7 @@ "@nmshd/consumption": "3.11.0", "@nmshd/content": "2.10.1", "@nmshd/crypto": "2.0.6", - "@nmshd/transport": "2.7.5", + "@nmshd/transport": "2.8.0", "ajv": "^8.16.0", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", diff --git a/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts b/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts index d88e029ed..a04b90473 100644 --- a/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts +++ b/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts @@ -12,6 +12,7 @@ import { LoadItemFromTruncatedReferenceResponse, LoadItemFromTruncatedReferenceUseCase, RegisterPushNotificationTokenRequest, + RegisterPushNotificationTokenResponse, RegisterPushNotificationTokenUseCase, SyncDatawalletRequest, SyncDatawalletUseCase, @@ -44,7 +45,7 @@ export class AccountFacade { return await this.getDeviceInfoUseCase.execute(); } - public async registerPushNotificationToken(request: RegisterPushNotificationTokenRequest): Promise> { + public async registerPushNotificationToken(request: RegisterPushNotificationTokenRequest): Promise> { return await this.registerPushNotificationTokenUseCase.execute(request); } diff --git a/packages/runtime/src/useCases/transport/account/RegisterPushNotificationToken.ts b/packages/runtime/src/useCases/transport/account/RegisterPushNotificationToken.ts index 89d09423d..3bb9a7cdb 100644 --- a/packages/runtime/src/useCases/transport/account/RegisterPushNotificationToken.ts +++ b/packages/runtime/src/useCases/transport/account/RegisterPushNotificationToken.ts @@ -10,13 +10,17 @@ export interface RegisterPushNotificationTokenRequest { environment?: "Development" | "Production"; } +export interface RegisterPushNotificationTokenResponse { + devicePushIdentifier: string; +} + class Validator extends SchemaValidator { public constructor(@Inject schemaRepository: SchemaRepository) { super(schemaRepository.getSchema("RegisterPushNotificationTokenRequest")); } } -export class RegisterPushNotificationTokenUseCase extends UseCase { +export class RegisterPushNotificationTokenUseCase extends UseCase { public constructor( @Inject private readonly accountController: AccountController, @Inject validator: Validator @@ -24,14 +28,14 @@ export class RegisterPushNotificationTokenUseCase extends UseCase> { - await this.accountController.registerPushNotificationToken({ + protected async executeInternal(request: RegisterPushNotificationTokenRequest): Promise> { + const result = await this.accountController.registerPushNotificationToken({ handle: request.handle, platform: request.platform, appId: request.appId, environment: request.environment }); - return Result.ok(undefined); + return Result.ok({ devicePushIdentifier: result.devicePushIdentifier }); } } diff --git a/packages/runtime/test/transport/account.test.ts b/packages/runtime/test/transport/account.test.ts index bf4d60013..c813db006 100644 --- a/packages/runtime/test/transport/account.test.ts +++ b/packages/runtime/test/transport/account.test.ts @@ -191,13 +191,14 @@ describe("Un-/RegisterPushNotificationToken", () => { test.each(["Development", "Production"])("register with valid enviroment", async (environment: any) => { const result = await sTransportServices.account.registerPushNotificationToken({ - handle: "handle", - platform: "platform", + handle: "handleLongerThan10Characters", + platform: "apns", appId: "appId", environment: environment }); expect(result).toBeSuccessful(); + expect(result.value.devicePushIdentifier).toMatch(/^DPI[a-zA-Z0-9]{17}$/); }); test("unregister", async () => { diff --git a/packages/transport/package.json b/packages/transport/package.json index 55c8122c7..dea7858db 100644 --- a/packages/transport/package.json +++ b/packages/transport/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/transport", - "version": "2.7.5", + "version": "2.8.0", "description": "The transport library handles backbone communication and content encryption.", "homepage": "https://enmeshed.eu", "repository": { diff --git a/packages/transport/src/modules/accounts/AccountController.ts b/packages/transport/src/modules/accounts/AccountController.ts index 2d884cdc6..95af89d90 100644 --- a/packages/transport/src/modules/accounts/AccountController.ts +++ b/packages/transport/src/modules/accounts/AccountController.ts @@ -407,8 +407,9 @@ export class AccountController { return device; } - public async registerPushNotificationToken(token: BackbonePutDevicesPushNotificationRequest): Promise { - await this.deviceAuthClient.registerPushNotificationToken(token); + public async registerPushNotificationToken(token: BackbonePutDevicesPushNotificationRequest): Promise<{ devicePushIdentifier: string }> { + const result = await this.deviceAuthClient.registerPushNotificationToken(token); + return result.value; } public async unregisterPushNotificationToken(): Promise { diff --git a/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts b/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts index 9a4621090..50ba697a1 100644 --- a/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts +++ b/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts @@ -14,6 +14,10 @@ export interface BackbonePutDevicesPushNotificationRequest { environment?: "Development" | "Production"; } +export interface BackbonePutDevicesPushNotificationResponse { + devicePushIdentifier: string; +} + export class DeviceAuthClient extends RESTClientAuthenticate { protected override _logDirective = RESTClientLogDirective.LogResponse; @@ -29,8 +33,8 @@ export class DeviceAuthClient extends RESTClientAuthenticate { return await this.delete(`/api/v1/Devices/${deviceId}`); } - public async registerPushNotificationToken(input: BackbonePutDevicesPushNotificationRequest): Promise> { - return await this.put("/api/v1/Devices/Self/PushNotifications", input); + public async registerPushNotificationToken(input: BackbonePutDevicesPushNotificationRequest): Promise> { + return await this.put("/api/v1/Devices/Self/PushNotifications", input); } public async unregisterPushNotificationToken(): Promise> {