diff --git a/packages/push-notifications/README.md b/packages/push-notifications/README.md index 974f0787..a99bb493 100644 --- a/packages/push-notifications/README.md +++ b/packages/push-notifications/README.md @@ -28,6 +28,6 @@


-Push Notifications plugin for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). +Push Notifications extension module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). It currently implements [Aries RFC 0734](https://github.com/hyperledger/aries-rfcs/tree/main/features/0734-push-notifications-fcm) (Firebase Cloud Messaging) and [Aries RFC 0699](https://github.com/hyperledger/aries-rfcs/tree/main/features/0699-push-notifications-apns) (Apple Push Notifications). For documentation on installation and usage of the Push Notifications package, refer to the [Docs](https://aries.js.org/guides/0.4/extensions/push-notifications). diff --git a/packages/push-notifications/samples/sample.ts b/packages/push-notifications/samples/sample.ts index 9ee1e527..1bd01be0 100644 --- a/packages/push-notifications/samples/sample.ts +++ b/packages/push-notifications/samples/sample.ts @@ -17,6 +17,15 @@ const run = async () => { // Gets the push notification device information located at the other agent behind the connection await agent.modules.pushNotificationsApns.getDeviceInfo('a-valid-connection') + + // Sends device info as response from a get-device-info message + await agent.modules.pushNotificationsApns.deviceInfo({ + connectionId: 'a-valid-connection', + threadId: 'get-device-info-msg-id', + deviceInfo: { + deviceToken: '123', + }, + }) } void run() diff --git a/packages/push-notifications/src/apns/PushNotificationsApnsApi.ts b/packages/push-notifications/src/apns/PushNotificationsApnsApi.ts index 7615b23b..0c48dcbd 100644 --- a/packages/push-notifications/src/apns/PushNotificationsApnsApi.ts +++ b/packages/push-notifications/src/apns/PushNotificationsApnsApi.ts @@ -53,14 +53,16 @@ export class PushNotificationsApnsApi { * Response for `push-notifications-apns/get-device-info` * * @param connectionId The connection ID string + * @param threadId get-device-info message ID * @param deviceInfo The APNS device info * @returns Promise */ - public async deviceInfo(connectionId: string, deviceInfo: ApnsDeviceInfo) { + public async deviceInfo(options: { connectionId: string; threadId: string; deviceInfo: ApnsDeviceInfo }) { + const { connectionId, threadId, deviceInfo } = options const connection = await this.connectionService.getById(this.agentContext, connectionId) connection.assertReady() - const message = this.pushNotificationsService.createDeviceInfo(deviceInfo) + const message = this.pushNotificationsService.createDeviceInfo({ threadId, deviceInfo }) const outbound = new OutboundMessageContext(message, { agentContext: this.agentContext, diff --git a/packages/push-notifications/src/apns/PushNotificationsApnsModule.ts b/packages/push-notifications/src/apns/PushNotificationsApnsModule.ts index c3c1aa8c..a0e27a1a 100644 --- a/packages/push-notifications/src/apns/PushNotificationsApnsModule.ts +++ b/packages/push-notifications/src/apns/PushNotificationsApnsModule.ts @@ -1,12 +1,16 @@ -import type { DependencyManager, Module } from '@aries-framework/core' +import type { DependencyManager, FeatureRegistry, Module } from '@aries-framework/core' + +import { Protocol } from '@aries-framework/core' import { PushNotificationsApnsApi } from './PushNotificationsApnsApi' import { PushNotificationsApnsService } from './PushNotificationsApnsService' import { PushNotificationsApnsDeviceInfoHandler, PushNotificationsApnsGetDeviceInfoHandler, + PushNotificationsApnsProblemReportHandler, PushNotificationsApnsSetDeviceInfoHandler, } from './handlers' +import { PushNotificationsApnsRole } from './models' /** * Module that exposes push notification get and set functionality @@ -14,13 +18,21 @@ import { export class PushNotificationsApnsModule implements Module { public readonly api = PushNotificationsApnsApi - public register(dependencyManager: DependencyManager): void { + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void { dependencyManager.registerContextScoped(PushNotificationsApnsApi) + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/push-notifications-apns/1.0', + roles: [PushNotificationsApnsRole.Sender, PushNotificationsApnsRole.Receiver], + }) + ) + dependencyManager.registerMessageHandlers([ new PushNotificationsApnsDeviceInfoHandler(), new PushNotificationsApnsGetDeviceInfoHandler(), new PushNotificationsApnsSetDeviceInfoHandler(), + new PushNotificationsApnsProblemReportHandler(), ]) dependencyManager.registerSingleton(PushNotificationsApnsService) diff --git a/packages/push-notifications/src/apns/PushNotificationsApnsService.ts b/packages/push-notifications/src/apns/PushNotificationsApnsService.ts index 45bf52b7..52730cd3 100644 --- a/packages/push-notifications/src/apns/PushNotificationsApnsService.ts +++ b/packages/push-notifications/src/apns/PushNotificationsApnsService.ts @@ -18,7 +18,8 @@ export class PushNotificationsApnsService { return new PushNotificationsApnsGetDeviceInfoMessage({}) } - public createDeviceInfo(deviceInfo: ApnsDeviceInfo) { - return new PushNotificationsApnsDeviceInfoMessage(deviceInfo) + public createDeviceInfo(options: { threadId: string; deviceInfo: ApnsDeviceInfo }) { + const { threadId, deviceInfo } = options + return new PushNotificationsApnsDeviceInfoMessage({ threadId, deviceToken: deviceInfo.deviceToken }) } } diff --git a/packages/push-notifications/src/apns/errors/PushNotificationsApnsProblemReportError.ts b/packages/push-notifications/src/apns/errors/PushNotificationsApnsProblemReportError.ts new file mode 100644 index 00000000..2c7ea1d3 --- /dev/null +++ b/packages/push-notifications/src/apns/errors/PushNotificationsApnsProblemReportError.ts @@ -0,0 +1,30 @@ +import type { PushNotificationsApnsProblemReportReason } from './PushNotificationsApnsProblemReportReason' +import type { ProblemReportErrorOptions } from '@aries-framework/core' + +import { ProblemReportError } from '@aries-framework/core' + +import { PushNotificationsApnsProblemReportMessage } from '../messages' + +/** + * @internal + */ +interface PushNotificationsApnsProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PushNotificationsApnsProblemReportReason +} + +/** + * @internal + */ +export class PushNotificationsApnsProblemReportError extends ProblemReportError { + public problemReport: PushNotificationsApnsProblemReportMessage + + public constructor(public message: string, { problemCode }: PushNotificationsApnsProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new PushNotificationsApnsProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/push-notifications/src/apns/errors/PushNotificationsApnsProblemReportReason.ts b/packages/push-notifications/src/apns/errors/PushNotificationsApnsProblemReportReason.ts new file mode 100644 index 00000000..4b163d2e --- /dev/null +++ b/packages/push-notifications/src/apns/errors/PushNotificationsApnsProblemReportReason.ts @@ -0,0 +1,11 @@ +/** + * Push Notification APNS errors discussed in RFC 0699. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0699-push-notifications-apns#set-device-info + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0699-push-notifications-apns#device-info + * @internal + */ +export enum PushNotificationsApnsProblemReportReason { + MissingValue = 'missing-value', + NotRegistered = 'not-registered-for-push-notifications', +} diff --git a/packages/push-notifications/src/apns/errors/index.ts b/packages/push-notifications/src/apns/errors/index.ts new file mode 100644 index 00000000..b54dc9d5 --- /dev/null +++ b/packages/push-notifications/src/apns/errors/index.ts @@ -0,0 +1,2 @@ +export * from './PushNotificationsApnsProblemReportReason' +export * from './PushNotificationsApnsProblemReportError' diff --git a/packages/push-notifications/src/apns/handlers/PushNotificationsApnsProblemReportHandler.ts b/packages/push-notifications/src/apns/handlers/PushNotificationsApnsProblemReportHandler.ts new file mode 100644 index 00000000..78763c36 --- /dev/null +++ b/packages/push-notifications/src/apns/handlers/PushNotificationsApnsProblemReportHandler.ts @@ -0,0 +1,18 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' + +import { PushNotificationsApnsProblemReportMessage } from '../messages' + +/** + * Handler for incoming push notification problem report messages + */ +export class PushNotificationsApnsProblemReportHandler implements MessageHandler { + public supportedMessages = [PushNotificationsApnsProblemReportMessage] + + /** + /* We don't really need to do anything with this at the moment + /* The result can be hooked into through the generic message processed event + */ + public async handle(inboundMessage: MessageHandlerInboundMessage) { + inboundMessage.assertReadyConnection() + } +} diff --git a/packages/push-notifications/src/apns/handlers/index.ts b/packages/push-notifications/src/apns/handlers/index.ts index c2996201..20d9c0e7 100644 --- a/packages/push-notifications/src/apns/handlers/index.ts +++ b/packages/push-notifications/src/apns/handlers/index.ts @@ -1,3 +1,4 @@ export { PushNotificationsApnsGetDeviceInfoHandler } from './PushNotificationsApnsGetDeviceInfoHandler' export { PushNotificationsApnsSetDeviceInfoHandler } from './PushNotificationsApnsSetDeviceInfoHandler' export { PushNotificationsApnsDeviceInfoHandler } from './PushNotificationsApnsDeviceInfoHandler' +export { PushNotificationsApnsProblemReportHandler } from './PushNotificationsApnsProblemReportHandler' diff --git a/packages/push-notifications/src/apns/messages/PushNotificationsApnsDeviceInfoMessage.ts b/packages/push-notifications/src/apns/messages/PushNotificationsApnsDeviceInfoMessage.ts index 1b856636..0d77e334 100644 --- a/packages/push-notifications/src/apns/messages/PushNotificationsApnsDeviceInfoMessage.ts +++ b/packages/push-notifications/src/apns/messages/PushNotificationsApnsDeviceInfoMessage.ts @@ -2,17 +2,18 @@ import type { ApnsDeviceInfo } from '../models' import { AgentMessage, IsValidMessageType, parseMessageType } from '@aries-framework/core' import { Expose } from 'class-transformer' -import { IsString } from 'class-validator' +import { IsString, ValidateIf } from 'class-validator' interface PushNotificationsApnsDeviceInfoOptions extends ApnsDeviceInfo { id?: string + threadId: string } /** * Message to send the apns device information from another agent for push notifications * This is used as a response for the `get-device-info` message * - * @todo ADD RFC + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0699-push-notifications-apns#device-info */ export class PushNotificationsApnsDeviceInfoMessage extends AgentMessage { public constructor(options: PushNotificationsApnsDeviceInfoOptions) { @@ -20,6 +21,7 @@ export class PushNotificationsApnsDeviceInfoMessage extends AgentMessage { if (options) { this.id = options.id ?? this.generateId() + this.setThread({ threadId: options.threadId }) this.deviceToken = options.deviceToken } } @@ -30,5 +32,6 @@ export class PushNotificationsApnsDeviceInfoMessage extends AgentMessage { @Expose({ name: 'device_token' }) @IsString() - public deviceToken!: string + @ValidateIf((object, value) => value !== null) + public deviceToken!: string | null } diff --git a/packages/push-notifications/src/apns/messages/PushNotificationsApnsGetDeviceInfoMessage.ts b/packages/push-notifications/src/apns/messages/PushNotificationsApnsGetDeviceInfoMessage.ts index 35b2f8db..d75a3bab 100644 --- a/packages/push-notifications/src/apns/messages/PushNotificationsApnsGetDeviceInfoMessage.ts +++ b/packages/push-notifications/src/apns/messages/PushNotificationsApnsGetDeviceInfoMessage.ts @@ -7,7 +7,7 @@ interface PushNotificationsApnsGetDeviceInfoOptions { /** * Message to get the apns device information from another agent for push notifications * - * @todo ADD RFC + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0699-push-notifications-apns#get-device-info */ export class PushNotificationsApnsGetDeviceInfoMessage extends AgentMessage { public constructor(options: PushNotificationsApnsGetDeviceInfoOptions) { diff --git a/packages/push-notifications/src/apns/messages/PushNotificationsApnsProblemReportMessage.ts b/packages/push-notifications/src/apns/messages/PushNotificationsApnsProblemReportMessage.ts new file mode 100644 index 00000000..17a81524 --- /dev/null +++ b/packages/push-notifications/src/apns/messages/PushNotificationsApnsProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '@aries-framework/core' + +import { IsValidMessageType, parseMessageType, ProblemReportMessage } from '@aries-framework/core' + +export type PushNotificationsApnsProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + * @internal + */ +export class PushNotificationsApnsProblemReportMessage extends ProblemReportMessage { + /** + * Create new ConnectionProblemReportMessage instance. + * @param options + */ + public constructor(options: PushNotificationsApnsProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(PushNotificationsApnsProblemReportMessage.type) + public readonly type = PushNotificationsApnsProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/push-notifications-apns/1.0/problem-report') +} diff --git a/packages/push-notifications/src/apns/messages/PushNotificationsApnsSetDeviceInfoMessage.ts b/packages/push-notifications/src/apns/messages/PushNotificationsApnsSetDeviceInfoMessage.ts index 528c0f8c..dd83c74e 100644 --- a/packages/push-notifications/src/apns/messages/PushNotificationsApnsSetDeviceInfoMessage.ts +++ b/packages/push-notifications/src/apns/messages/PushNotificationsApnsSetDeviceInfoMessage.ts @@ -2,7 +2,7 @@ import type { ApnsDeviceInfo } from '../models' import { AgentMessage, IsValidMessageType, parseMessageType } from '@aries-framework/core' import { Expose } from 'class-transformer' -import { IsString } from 'class-validator' +import { IsString, ValidateIf } from 'class-validator' interface PushNotificationsApnsSetDeviceInfoOptions extends ApnsDeviceInfo { id?: string @@ -11,7 +11,7 @@ interface PushNotificationsApnsSetDeviceInfoOptions extends ApnsDeviceInfo { /** * Message to set the apns device information at another agent for push notifications * - * @todo ADD RFC + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0699-push-notifications-apns#set-device-info */ export class PushNotificationsApnsSetDeviceInfoMessage extends AgentMessage { public constructor(options: PushNotificationsApnsSetDeviceInfoOptions) { @@ -25,7 +25,8 @@ export class PushNotificationsApnsSetDeviceInfoMessage extends AgentMessage { @Expose({ name: 'device_token' }) @IsString() - public deviceToken!: string + @ValidateIf((object, value) => value !== null) + public deviceToken!: string | null @IsValidMessageType(PushNotificationsApnsSetDeviceInfoMessage.type) public readonly type = PushNotificationsApnsSetDeviceInfoMessage.type.messageTypeUri diff --git a/packages/push-notifications/src/apns/messages/index.ts b/packages/push-notifications/src/apns/messages/index.ts index 39dd8163..6a92ba33 100644 --- a/packages/push-notifications/src/apns/messages/index.ts +++ b/packages/push-notifications/src/apns/messages/index.ts @@ -1,3 +1,4 @@ export { PushNotificationsApnsGetDeviceInfoMessage } from './PushNotificationsApnsGetDeviceInfoMessage' export { PushNotificationsApnsSetDeviceInfoMessage } from './PushNotificationsApnsSetDeviceInfoMessage' export { PushNotificationsApnsDeviceInfoMessage } from './PushNotificationsApnsDeviceInfoMessage' +export { PushNotificationsApnsProblemReportMessage } from './PushNotificationsApnsProblemReportMessage' diff --git a/packages/push-notifications/src/apns/models/ApnsDeviceInfo.ts b/packages/push-notifications/src/apns/models/ApnsDeviceInfo.ts index 6e7013c1..e8db2a7d 100644 --- a/packages/push-notifications/src/apns/models/ApnsDeviceInfo.ts +++ b/packages/push-notifications/src/apns/models/ApnsDeviceInfo.ts @@ -1,3 +1,3 @@ export type ApnsDeviceInfo = { - deviceToken: string + deviceToken: string | null } diff --git a/packages/push-notifications/src/apns/models/PushNotificationsApnsRole.ts b/packages/push-notifications/src/apns/models/PushNotificationsApnsRole.ts new file mode 100644 index 00000000..5900541f --- /dev/null +++ b/packages/push-notifications/src/apns/models/PushNotificationsApnsRole.ts @@ -0,0 +1,10 @@ +/** + * Push Notification FCM roles based on the flow defined in RFC 0699. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0699-push-notifications-apns#roles + * @public + */ +export enum PushNotificationsApnsRole { + Sender = 'notification-sender', + Receiver = 'notification-receiver', +} diff --git a/packages/push-notifications/src/apns/models/index.ts b/packages/push-notifications/src/apns/models/index.ts index c8e35f78..20a09fbc 100644 --- a/packages/push-notifications/src/apns/models/index.ts +++ b/packages/push-notifications/src/apns/models/index.ts @@ -1 +1,2 @@ export * from './ApnsDeviceInfo' +export * from './PushNotificationsApnsRole' diff --git a/packages/push-notifications/src/fcm/PushNotificationsFcmApi.ts b/packages/push-notifications/src/fcm/PushNotificationsFcmApi.ts index 08f51a63..a8271596 100644 --- a/packages/push-notifications/src/fcm/PushNotificationsFcmApi.ts +++ b/packages/push-notifications/src/fcm/PushNotificationsFcmApi.ts @@ -54,14 +54,16 @@ export class PushNotificationsFcmApi { * Response for `push-notifications-fcm/get-device-info` * * @param connectionId The connection ID string + * @param threadId get-device-info message ID * @param deviceInfo The FCM device info * @returns Promise */ - public async deviceInfo(connectionId: string, deviceInfo: FcmDeviceInfo) { + public async deviceInfo(options: { connectionId: string; threadId: string; deviceInfo: FcmDeviceInfo }) { + const { connectionId, threadId, deviceInfo } = options const connection = await this.connectionService.getById(this.agentContext, connectionId) connection.assertReady() - const message = this.pushNotificationsService.createDeviceInfo(deviceInfo) + const message = this.pushNotificationsService.createDeviceInfo({ threadId, deviceInfo }) const outbound = new OutboundMessageContext(message, { agentContext: this.agentContext, diff --git a/packages/push-notifications/src/fcm/PushNotificationsFcmModule.ts b/packages/push-notifications/src/fcm/PushNotificationsFcmModule.ts index c85de8ce..1c3f563f 100644 --- a/packages/push-notifications/src/fcm/PushNotificationsFcmModule.ts +++ b/packages/push-notifications/src/fcm/PushNotificationsFcmModule.ts @@ -1,12 +1,16 @@ -import type { DependencyManager, Module } from '@aries-framework/core' +import type { DependencyManager, FeatureRegistry, Module } from '@aries-framework/core' + +import { Protocol } from '@aries-framework/core' import { PushNotificationsFcmApi } from './PushNotificationsFcmApi' import { PushNotificationsFcmService } from './PushNotificationsFcmService' import { PushNotificationsFcmDeviceInfoHandler, PushNotificationsFcmGetDeviceInfoHandler, + PushNotificationsFcmProblemReportHandler, PushNotificationsFcmSetDeviceInfoHandler, } from './handlers' +import { PushNotificationsFcmRole } from './models' /** * Module that exposes push notification get and set functionality @@ -14,15 +18,23 @@ import { export class PushNotificationsFcmModule implements Module { public readonly api = PushNotificationsFcmApi - public register(dependencyManager: DependencyManager): void { + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void { dependencyManager.registerContextScoped(PushNotificationsFcmApi) dependencyManager.registerSingleton(PushNotificationsFcmService) + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/push-notifications-fcm/1.0', + roles: [PushNotificationsFcmRole.Sender, PushNotificationsFcmRole.Receiver], + }) + ) + dependencyManager.registerMessageHandlers([ new PushNotificationsFcmDeviceInfoHandler(), new PushNotificationsFcmGetDeviceInfoHandler(), new PushNotificationsFcmSetDeviceInfoHandler(), + new PushNotificationsFcmProblemReportHandler(), ]) } } diff --git a/packages/push-notifications/src/fcm/PushNotificationsFcmService.ts b/packages/push-notifications/src/fcm/PushNotificationsFcmService.ts index 88ff0165..677d9d84 100644 --- a/packages/push-notifications/src/fcm/PushNotificationsFcmService.ts +++ b/packages/push-notifications/src/fcm/PushNotificationsFcmService.ts @@ -1,7 +1,10 @@ import type { FcmDeviceInfo } from './models/FcmDeviceInfo' +import type { InboundMessageContext } from '@aries-framework/core' +import { AriesFrameworkError } from '@aries-framework/core' import { Lifecycle, scoped } from 'tsyringe' +import { PushNotificationsFcmProblemReportError, PushNotificationsFcmProblemReportReason } from './errors' import { PushNotificationsFcmSetDeviceInfoMessage, PushNotificationsFcmGetDeviceInfoMessage, @@ -11,6 +14,12 @@ import { @scoped(Lifecycle.ContainerScoped) export class PushNotificationsFcmService { public createSetDeviceInfo(deviceInfo: FcmDeviceInfo) { + if ( + (deviceInfo.deviceToken === null && deviceInfo.devicePlatform !== null) || + (deviceInfo.deviceToken !== null && deviceInfo.devicePlatform === null) + ) + throw new AriesFrameworkError('Both or none of deviceToken and devicePlatform must be null') + return new PushNotificationsFcmSetDeviceInfoMessage(deviceInfo) } @@ -18,7 +27,29 @@ export class PushNotificationsFcmService { return new PushNotificationsFcmGetDeviceInfoMessage({}) } - public createDeviceInfo(deviceInfo: FcmDeviceInfo) { - return new PushNotificationsFcmDeviceInfoMessage(deviceInfo) + public createDeviceInfo(options: { threadId: string; deviceInfo: FcmDeviceInfo }) { + const { threadId, deviceInfo } = options + if ( + (deviceInfo.deviceToken === null && deviceInfo.devicePlatform !== null) || + (deviceInfo.deviceToken !== null && deviceInfo.devicePlatform === null) + ) + throw new AriesFrameworkError('Both or none of deviceToken and devicePlatform must be null') + + return new PushNotificationsFcmDeviceInfoMessage({ + threadId, + deviceToken: deviceInfo.deviceToken, + devicePlatform: deviceInfo.devicePlatform, + }) + } + + public processSetDeviceInfo(messageContext: InboundMessageContext) { + const { message } = messageContext + if ( + (message.deviceToken === null && message.devicePlatform !== null) || + (message.deviceToken !== null && message.devicePlatform === null) + ) + throw new PushNotificationsFcmProblemReportError('Both or none of deviceToken and devicePlatform must be null', { + problemCode: PushNotificationsFcmProblemReportReason.MissingValue, + }) } } diff --git a/packages/push-notifications/src/fcm/errors/PushNotificationsFcmProblemReportError.ts b/packages/push-notifications/src/fcm/errors/PushNotificationsFcmProblemReportError.ts new file mode 100644 index 00000000..c1e973a0 --- /dev/null +++ b/packages/push-notifications/src/fcm/errors/PushNotificationsFcmProblemReportError.ts @@ -0,0 +1,30 @@ +import type { PushNotificationsFcmProblemReportReason } from './PushNotificationsFcmProblemReportReason' +import type { ProblemReportErrorOptions } from '@aries-framework/core' + +import { ProblemReportError } from '@aries-framework/core' + +import { PushNotificationsFcmProblemReportMessage } from '../messages' + +/** + * @internal + */ +interface PushNotificationsFcmProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PushNotificationsFcmProblemReportReason +} + +/** + * @internal + */ +export class PushNotificationsFcmProblemReportError extends ProblemReportError { + public problemReport: PushNotificationsFcmProblemReportMessage + + public constructor(public message: string, { problemCode }: PushNotificationsFcmProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new PushNotificationsFcmProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/push-notifications/src/fcm/errors/PushNotificationsFcmProblemReportReason.ts b/packages/push-notifications/src/fcm/errors/PushNotificationsFcmProblemReportReason.ts new file mode 100644 index 00000000..fd6f3446 --- /dev/null +++ b/packages/push-notifications/src/fcm/errors/PushNotificationsFcmProblemReportReason.ts @@ -0,0 +1,11 @@ +/** + * Push Notification FCM errors discussed in RFC 0734. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0734-push-notifications-fcm#set-device-info + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0734-push-notifications-fcm#device-info + * @internal + */ +export enum PushNotificationsFcmProblemReportReason { + MissingValue = 'missing-value', + NotRegistered = 'not-registered-for-push-notifications', +} diff --git a/packages/push-notifications/src/fcm/errors/index.ts b/packages/push-notifications/src/fcm/errors/index.ts new file mode 100644 index 00000000..a1155d8a --- /dev/null +++ b/packages/push-notifications/src/fcm/errors/index.ts @@ -0,0 +1,2 @@ +export * from './PushNotificationsFcmProblemReportReason' +export * from './PushNotificationsFcmProblemReportError' diff --git a/packages/push-notifications/src/fcm/handlers/PushNotificationsFcmProblemReportHandler.ts b/packages/push-notifications/src/fcm/handlers/PushNotificationsFcmProblemReportHandler.ts new file mode 100644 index 00000000..c14c622d --- /dev/null +++ b/packages/push-notifications/src/fcm/handlers/PushNotificationsFcmProblemReportHandler.ts @@ -0,0 +1,18 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' + +import { PushNotificationsFcmProblemReportMessage } from '../messages' + +/** + * Handler for incoming push notification problem report messages + */ +export class PushNotificationsFcmProblemReportHandler implements MessageHandler { + public supportedMessages = [PushNotificationsFcmProblemReportMessage] + + /** + /* We don't really need to do anything with this at the moment + /* The result can be hooked into through the generic message processed event + */ + public async handle(inboundMessage: MessageHandlerInboundMessage) { + inboundMessage.assertReadyConnection() + } +} diff --git a/packages/push-notifications/src/fcm/handlers/PushNotificationsFcmSetDeviceInfoHandler.ts b/packages/push-notifications/src/fcm/handlers/PushNotificationsFcmSetDeviceInfoHandler.ts index e631c98e..d94d20dd 100644 --- a/packages/push-notifications/src/fcm/handlers/PushNotificationsFcmSetDeviceInfoHandler.ts +++ b/packages/push-notifications/src/fcm/handlers/PushNotificationsFcmSetDeviceInfoHandler.ts @@ -1,5 +1,6 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' +import { PushNotificationsFcmService } from '../PushNotificationsFcmService' import { PushNotificationsFcmSetDeviceInfoMessage } from '../messages' /** @@ -9,10 +10,15 @@ export class PushNotificationsFcmSetDeviceInfoHandler implements MessageHandler public supportedMessages = [PushNotificationsFcmSetDeviceInfoMessage] /** - /* We don't really need to do anything with this at the moment + /* Only perform checks about message fields + /* /* The result can be hooked into through the generic message processed event */ public async handle(inboundMessage: MessageHandlerInboundMessage) { inboundMessage.assertReadyConnection() + + const pushNotificationsFcmService = + inboundMessage.agentContext.dependencyManager.resolve(PushNotificationsFcmService) + pushNotificationsFcmService.processSetDeviceInfo(inboundMessage) } } diff --git a/packages/push-notifications/src/fcm/handlers/index.ts b/packages/push-notifications/src/fcm/handlers/index.ts index cabdaa0d..ea2c5409 100644 --- a/packages/push-notifications/src/fcm/handlers/index.ts +++ b/packages/push-notifications/src/fcm/handlers/index.ts @@ -1,3 +1,4 @@ export { PushNotificationsFcmDeviceInfoHandler } from './PushNotificationsFcmDeviceInfoHandler' export { PushNotificationsFcmGetDeviceInfoHandler } from './PushNotificationsFcmGetDeviceInfoHandler' export { PushNotificationsFcmSetDeviceInfoHandler } from './PushNotificationsFcmSetDeviceInfoHandler' +export { PushNotificationsFcmProblemReportHandler } from './PushNotificationsFcmProblemReportHandler' diff --git a/packages/push-notifications/src/fcm/messages/PushNotificationsFcmDeviceInfoMessage.ts b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmDeviceInfoMessage.ts index dbad8883..6b333469 100644 --- a/packages/push-notifications/src/fcm/messages/PushNotificationsFcmDeviceInfoMessage.ts +++ b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmDeviceInfoMessage.ts @@ -2,17 +2,18 @@ import type { FcmDeviceInfo } from '../models' import { AgentMessage, IsValidMessageType, parseMessageType } from '@aries-framework/core' import { Expose } from 'class-transformer' -import { IsString } from 'class-validator' +import { IsString, ValidateIf } from 'class-validator' interface PushNotificationsFcmDeviceInfoOptions extends FcmDeviceInfo { id?: string + threadId: string } /** * Message to send the fcm device information from another agent for push notifications * This is used as a response for the `get-device-info` message * - * @todo ADD RFC + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0734-push-notifications-fcm#device-info */ export class PushNotificationsFcmDeviceInfoMessage extends AgentMessage { public constructor(options: PushNotificationsFcmDeviceInfoOptions) { @@ -20,13 +21,21 @@ export class PushNotificationsFcmDeviceInfoMessage extends AgentMessage { if (options) { this.id = options.id ?? this.generateId() + this.setThread({ threadId: options.threadId }) this.deviceToken = options.deviceToken + this.devicePlatform = options.devicePlatform } } @Expose({ name: 'device_token' }) @IsString() - public deviceToken!: string + @ValidateIf((object, value) => value !== null) + public deviceToken!: string | null + + @Expose({ name: 'device_platform' }) + @IsString() + @ValidateIf((object, value) => value !== null) + public devicePlatform!: string | null @IsValidMessageType(PushNotificationsFcmDeviceInfoMessage.type) public readonly type = PushNotificationsFcmDeviceInfoMessage.type.messageTypeUri diff --git a/packages/push-notifications/src/fcm/messages/PushNotificationsFcmGetDeviceInfoMessage.ts b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmGetDeviceInfoMessage.ts index dc21647e..c20ebca8 100644 --- a/packages/push-notifications/src/fcm/messages/PushNotificationsFcmGetDeviceInfoMessage.ts +++ b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmGetDeviceInfoMessage.ts @@ -7,7 +7,7 @@ interface PushNotificationsFcmGetDeviceInfoOptions { /** * Message to get fcm the device information from another agent for push notifications * - * @todo ADD RFC + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0734-push-notifications-fcm#get-device-info */ export class PushNotificationsFcmGetDeviceInfoMessage extends AgentMessage { public constructor(options: PushNotificationsFcmGetDeviceInfoOptions) { diff --git a/packages/push-notifications/src/fcm/messages/PushNotificationsFcmProblemReportMessage.ts b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmProblemReportMessage.ts new file mode 100644 index 00000000..df86cb86 --- /dev/null +++ b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '@aries-framework/core' + +import { IsValidMessageType, parseMessageType, ProblemReportMessage } from '@aries-framework/core' + +export type PushNotificationsFcmProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + * @internal + */ +export class PushNotificationsFcmProblemReportMessage extends ProblemReportMessage { + /** + * Create new ConnectionProblemReportMessage instance. + * @param options + */ + public constructor(options: PushNotificationsFcmProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(PushNotificationsFcmProblemReportMessage.type) + public readonly type = PushNotificationsFcmProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/push-notifications-fcm/1.0/problem-report') +} diff --git a/packages/push-notifications/src/fcm/messages/PushNotificationsFcmSetDeviceInfoMessage.ts b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmSetDeviceInfoMessage.ts index 2b155ccf..8115b213 100644 --- a/packages/push-notifications/src/fcm/messages/PushNotificationsFcmSetDeviceInfoMessage.ts +++ b/packages/push-notifications/src/fcm/messages/PushNotificationsFcmSetDeviceInfoMessage.ts @@ -2,7 +2,7 @@ import type { FcmDeviceInfo } from '../models' import { AgentMessage, IsValidMessageType, parseMessageType } from '@aries-framework/core' import { Expose } from 'class-transformer' -import { IsString } from 'class-validator' +import { IsString, ValidateIf } from 'class-validator' interface PushNotificationsFcmSetDeviceInfoOptions extends FcmDeviceInfo { id?: string @@ -11,7 +11,7 @@ interface PushNotificationsFcmSetDeviceInfoOptions extends FcmDeviceInfo { /** * Message to set the fcm device information at another agent for push notifications * - * @todo ADD RFC + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0734-push-notifications-fcm#set-device-info */ export class PushNotificationsFcmSetDeviceInfoMessage extends AgentMessage { public constructor(options: PushNotificationsFcmSetDeviceInfoOptions) { @@ -20,12 +20,19 @@ export class PushNotificationsFcmSetDeviceInfoMessage extends AgentMessage { if (options) { this.id = options.id ?? this.generateId() this.deviceToken = options.deviceToken + this.devicePlatform = options.devicePlatform } } @Expose({ name: 'device_token' }) @IsString() - public deviceToken!: string + @ValidateIf((object, value) => value !== null) + public deviceToken!: string | null + + @Expose({ name: 'device_platform' }) + @IsString() + @ValidateIf((object, value) => value !== null) + public devicePlatform!: string | null @IsValidMessageType(PushNotificationsFcmSetDeviceInfoMessage.type) public readonly type = PushNotificationsFcmSetDeviceInfoMessage.type.messageTypeUri diff --git a/packages/push-notifications/src/fcm/messages/index.ts b/packages/push-notifications/src/fcm/messages/index.ts index f064c187..59f3aee1 100644 --- a/packages/push-notifications/src/fcm/messages/index.ts +++ b/packages/push-notifications/src/fcm/messages/index.ts @@ -1,3 +1,4 @@ export { PushNotificationsFcmDeviceInfoMessage } from './PushNotificationsFcmDeviceInfoMessage' export { PushNotificationsFcmGetDeviceInfoMessage } from './PushNotificationsFcmGetDeviceInfoMessage' export { PushNotificationsFcmSetDeviceInfoMessage } from './PushNotificationsFcmSetDeviceInfoMessage' +export { PushNotificationsFcmProblemReportMessage } from './PushNotificationsFcmProblemReportMessage' diff --git a/packages/push-notifications/src/fcm/models/FcmDeviceInfo.ts b/packages/push-notifications/src/fcm/models/FcmDeviceInfo.ts index f0aad43f..644c230d 100644 --- a/packages/push-notifications/src/fcm/models/FcmDeviceInfo.ts +++ b/packages/push-notifications/src/fcm/models/FcmDeviceInfo.ts @@ -1,3 +1,4 @@ export type FcmDeviceInfo = { - deviceToken: string + deviceToken: string | null + devicePlatform: string | null } diff --git a/packages/push-notifications/src/fcm/models/PushNotificationsFcmRole.ts b/packages/push-notifications/src/fcm/models/PushNotificationsFcmRole.ts new file mode 100644 index 00000000..e94cbc7d --- /dev/null +++ b/packages/push-notifications/src/fcm/models/PushNotificationsFcmRole.ts @@ -0,0 +1,10 @@ +/** + * Push Notification FCM roles based on the flow defined in RFC 0734. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0734-push-notifications-fcm#roles + * @public + */ +export enum PushNotificationsFcmRole { + Sender = 'notification-sender', + Receiver = 'notification-receiver', +} diff --git a/packages/push-notifications/src/fcm/models/index.ts b/packages/push-notifications/src/fcm/models/index.ts index 4cd264aa..a30d50c8 100644 --- a/packages/push-notifications/src/fcm/models/index.ts +++ b/packages/push-notifications/src/fcm/models/index.ts @@ -1 +1,2 @@ export * from './FcmDeviceInfo' +export * from './PushNotificationsFcmRole' diff --git a/packages/push-notifications/tests/pushNotificationsApnsService.test.ts b/packages/push-notifications/tests/pushNotificationsApnsService.test.ts index 678dda61..d3fecce9 100644 --- a/packages/push-notifications/tests/pushNotificationsApnsService.test.ts +++ b/packages/push-notifications/tests/pushNotificationsApnsService.test.ts @@ -25,16 +25,22 @@ describe('Push Notifications apns', () => { describe('Create apns set push notification message', () => { test('Should create a valid https://didcomm.org/push-notifications-apns/1.0/device-info message ', async () => { const message = pushNotificationsApnsService.createDeviceInfo({ - deviceToken: '1234-1234-1234-1234', + threadId: '5678-5678-5678-5678', + deviceInfo: { + deviceToken: '1234-1234-1234-1234', + }, }) const jsonMessage = JsonTransformer.toJSON(message) - expect(jsonMessage).toEqual({ - '@id': expect.any(String), - '@type': 'https://didcomm.org/push-notifications-apns/1.0/device-info', - device_token: '1234-1234-1234-1234', - }) + expect(jsonMessage).toEqual( + expect.objectContaining({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/push-notifications-apns/1.0/device-info', + device_token: '1234-1234-1234-1234', + '~thread': expect.objectContaining({ thid: '5678-5678-5678-5678' }), + }) + ) }) }) describe('Create apns set push notification message', () => { diff --git a/packages/push-notifications/tests/pushNotificationsFcmService.test.ts b/packages/push-notifications/tests/pushNotificationsFcmService.test.ts index 43e8e427..a343c1c7 100644 --- a/packages/push-notifications/tests/pushNotificationsFcmService.test.ts +++ b/packages/push-notifications/tests/pushNotificationsFcmService.test.ts @@ -1,8 +1,9 @@ -import type { Agent } from '@aries-framework/core' +import type { Agent, AgentMessage, ConnectionRecord } from '@aries-framework/core' -import { JsonTransformer } from '@aries-framework/core' +import { AgentContext, DependencyManager, JsonTransformer } from '@aries-framework/core' import { PushNotificationsFcmService } from '../src/fcm/PushNotificationsFcmService' +import { PushNotificationsFcmProblemReportError } from '../src/fcm/errors' import { setupAgentFcm } from './utils/agent' @@ -23,10 +24,11 @@ describe('Push Notifications Fcm ', () => { await agent.wallet.delete() }) - describe('Create fcm set push notification message', () => { - test('Should create a valid https://didcomm.org/push-notifications-fcm/1.0/set-device-info message ', async () => { + describe('Create fcm set-device-info message', () => { + test('Should create a valid message with both token and platform', async () => { const message = pushNotificationsService.createSetDeviceInfo({ deviceToken: '1234-1234-1234-1234', + devicePlatform: 'android', }) const jsonMessage = JsonTransformer.toJSON(message) @@ -35,12 +37,45 @@ describe('Push Notifications Fcm ', () => { '@id': expect.any(String), '@type': 'https://didcomm.org/push-notifications-fcm/1.0/set-device-info', device_token: '1234-1234-1234-1234', + device_platform: 'android', }) }) + + test('Should create a valid message without token and platform ', async () => { + const message = pushNotificationsService.createSetDeviceInfo({ + deviceToken: null, + devicePlatform: null, + }) + + const jsonMessage = JsonTransformer.toJSON(message) + + expect(jsonMessage).toEqual({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/push-notifications-fcm/1.0/set-device-info', + device_token: null, + device_platform: null, + }) + }) + + test('Should throw error if either token or platform are missing', async () => { + expect(() => + pushNotificationsService.createSetDeviceInfo({ + deviceToken: 'something', + devicePlatform: null, + }) + ).toThrow('Both or none of deviceToken and devicePlatform must be null') + + expect(() => + pushNotificationsService.createSetDeviceInfo({ + deviceToken: null, + devicePlatform: 'something', + }) + ).toThrow('Both or none of deviceToken and devicePlatform must be null') + }) }) - describe('Create fcm get device info message', () => { - test('Should create a valid https://didcomm.org/push-notifications-fcm/1.0/get-device-info message ', async () => { + describe('Create fcm get-device-info message', () => { + test('Should create a valid message ', async () => { const message = pushNotificationsService.createGetDeviceInfo() const jsonMessage = JsonTransformer.toJSON(message) @@ -52,19 +87,114 @@ describe('Push Notifications Fcm ', () => { }) }) - describe('Create fcm device info message', () => { - test('Should create a valid https://didcomm.org/push-notifications-fcm/1.0/device-info message ', async () => { + describe('Create fcm device-info message', () => { + test('Should create a valid message with both token and platform', async () => { const message = pushNotificationsService.createDeviceInfo({ - deviceToken: '1234-1234-1234-1234', + threadId: '5678-5678-5678-5678', + deviceInfo: { + deviceToken: '1234-1234-1234-1234', + devicePlatform: 'android', + }, }) const jsonMessage = JsonTransformer.toJSON(message) - expect(jsonMessage).toEqual({ - '@id': expect.any(String), - '@type': 'https://didcomm.org/push-notifications-fcm/1.0/device-info', - device_token: '1234-1234-1234-1234', + expect(jsonMessage).toEqual( + expect.objectContaining({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/push-notifications-fcm/1.0/device-info', + device_token: '1234-1234-1234-1234', + device_platform: 'android', + '~thread': expect.objectContaining({ thid: '5678-5678-5678-5678' }), + }) + ) + }) + + test('Should create a valid message without token and platform ', async () => { + const message = pushNotificationsService.createDeviceInfo({ + threadId: '5678-5678-5678-5678', + deviceInfo: { + deviceToken: null, + devicePlatform: null, + }, }) + + const jsonMessage = JsonTransformer.toJSON(message) + + expect(jsonMessage).toEqual( + expect.objectContaining({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/push-notifications-fcm/1.0/device-info', + device_token: null, + device_platform: null, + '~thread': expect.objectContaining({ thid: '5678-5678-5678-5678' }), + }) + ) + }) + + test('Should throw error if either token or platform are missing', async () => { + expect(() => + pushNotificationsService.createDeviceInfo({ + threadId: '5678-5678-5678-5678', + deviceInfo: { + deviceToken: 'something', + devicePlatform: null, + }, + }) + ).toThrow('Both or none of deviceToken and devicePlatform must be null') + + expect(() => + pushNotificationsService.createDeviceInfo({ + threadId: '5678-5678-5678-5678', + deviceInfo: { + deviceToken: null, + devicePlatform: 'something', + }, + }) + ).toThrow('Both or none of deviceToken and devicePlatform must be null') + }) + }) + + describe('Process fcm set-device-info message', () => { + test('Should throw if one of token and platform are missing', async () => { + const message = pushNotificationsService.createSetDeviceInfo({ + deviceToken: '1234-1234-1234-1234', + devicePlatform: 'android', + }) + + message.devicePlatform = null + expect(() => pushNotificationsService.processSetDeviceInfo(createInboundMessageContext(message))).toThrow( + PushNotificationsFcmProblemReportError + ) + + message.deviceToken = null + expect(() => pushNotificationsService.processSetDeviceInfo(createInboundMessageContext(message))).not.toThrow( + PushNotificationsFcmProblemReportError + ) + + message.devicePlatform = 'something' + expect(() => pushNotificationsService.processSetDeviceInfo(createInboundMessageContext(message))).toThrow( + PushNotificationsFcmProblemReportError + ) }) }) }) + +function createInboundMessageContext(message: T) { + return { + agentContext: new AgentContext({ dependencyManager: new DependencyManager(), contextCorrelationId: '' }), + message, + assertReadyConnection: function (): ConnectionRecord { + throw new Error('Function not implemented.') + }, + toJSON: function (): { + message: T + recipientKey: string | undefined + senderKey: string | undefined + sessionId: string | undefined + agentContext: { contextCorrelationId: string } + } { + throw new Error('Function not implemented.') + }, + } +}