diff --git a/apps/notifications/src/__tests__/mappings/trackCollaboratorAccept.test.ts b/apps/notifications/src/__tests__/mappings/trackCollaboratorAccept.test.ts
new file mode 100644
index 0000000..9355d56
--- /dev/null
+++ b/apps/notifications/src/__tests__/mappings/trackCollaboratorAccept.test.ts
@@ -0,0 +1,96 @@
+import { expect, jest, test } from '@jest/globals'
+import { Processor } from '../../main'
+import * as sns from '../../sns'
+import * as web from '../../web'
+
+import {
+ createTracks,
+ createUsers,
+ insertMobileDevices,
+ insertMobileSettings,
+ insertNotifications,
+ resetTests,
+ setUserEmailAndSettings,
+ setupTest
+} from '../../utils/populateDB'
+
+describe('Track Collaborator Accept', () => {
+ let processor: Processor
+
+ const sendPushNotificationSpy = jest
+ .spyOn(sns, 'sendPushNotification')
+ .mockImplementation(() => Promise.resolve({ endpointDisabled: false }))
+
+ const sendBrowserNotificationSpy = jest
+ .spyOn(web, 'sendBrowserNotification')
+ .mockImplementation(() => Promise.resolve(3))
+
+ beforeEach(async () => {
+ const setup = await setupTest()
+ processor = setup.processor
+ })
+
+ afterEach(async () => {
+ await resetTests(processor)
+ })
+
+ test('Process push notification for track collaborator accept', async () => {
+ // user 1 = collaborator (accepted), user 2 = inviter / track owner (recipient)
+ await createUsers(processor.discoveryDB, [{ user_id: 1 }, { user_id: 2 }])
+ await createTracks(processor.discoveryDB, [
+ { track_id: 10, owner_id: 2 }
+ ])
+ await setUserEmailAndSettings(processor.identityDB, 'live', 2)
+
+ await insertNotifications(processor.discoveryDB, [
+ {
+ id: 1,
+ specifier: '1',
+ group_id:
+ 'track_collaborator_accept:track_id:10:collaborator_user_id:1:inviter_user_id:2',
+ type: 'track_collaborator_accept',
+ data: {
+ track_id: 10,
+ collaborator_user_id: 1,
+ inviter_user_id: 2
+ },
+ user_ids: [2]
+ }
+ ])
+
+ await insertMobileSettings(processor.identityDB, [{ userId: 2 }])
+ await insertMobileDevices(processor.identityDB, [{ userId: 2 }])
+
+ const pending = processor.listener.takePending()
+ expect(pending?.appNotifications).toHaveLength(1)
+
+ const title = 'Collaboration Accepted'
+ const body =
+ 'user_1 accepted your invitation to collaborate on track_title_10.'
+ await processor.appNotificationsProcessor.process(pending.appNotifications)
+
+ expect(sendPushNotificationSpy).toHaveBeenCalledWith(
+ {
+ type: 'ios',
+ targetARN: 'arn:2',
+ badgeCount: 1
+ },
+ expect.objectContaining({
+ title,
+ body,
+ data: expect.objectContaining({
+ type: 'TrackCollaboratorAccept',
+ entityId: 10
+ })
+ })
+ )
+
+ expect(sendBrowserNotificationSpy).toHaveBeenCalledWith(
+ true,
+ expect.any(Object),
+ 2,
+ title,
+ body
+ )
+ })
+})
diff --git a/apps/notifications/src/__tests__/mappings/trackCollaboratorInvite.test.ts b/apps/notifications/src/__tests__/mappings/trackCollaboratorInvite.test.ts
new file mode 100644
index 0000000..eacc09f
--- /dev/null
+++ b/apps/notifications/src/__tests__/mappings/trackCollaboratorInvite.test.ts
@@ -0,0 +1,95 @@
+import { expect, jest, test } from '@jest/globals'
+import { Processor } from '../../main'
+import * as sns from '../../sns'
+import * as web from '../../web'
+
+import {
+ createTracks,
+ createUsers,
+ insertMobileDevices,
+ insertMobileSettings,
+ insertNotifications,
+ resetTests,
+ setUserEmailAndSettings,
+ setupTest
+} from '../../utils/populateDB'
+
+describe('Track Collaborator Invite', () => {
+ let processor: Processor
+
+ const sendPushNotificationSpy = jest
+ .spyOn(sns, 'sendPushNotification')
+ .mockImplementation(() => Promise.resolve({ endpointDisabled: false }))
+
+ const sendBrowserNotificationSpy = jest
+ .spyOn(web, 'sendBrowserNotification')
+ .mockImplementation(() => Promise.resolve(3))
+
+ beforeEach(async () => {
+ const setup = await setupTest()
+ processor = setup.processor
+ })
+
+ afterEach(async () => {
+ await resetTests(processor)
+ })
+
+ test('Process push notification for track collaborator invite', async () => {
+ // user 1 = invited collaborator (recipient), user 2 = inviter / track owner
+ await createUsers(processor.discoveryDB, [{ user_id: 1 }, { user_id: 2 }])
+ await createTracks(processor.discoveryDB, [
+ { track_id: 10, owner_id: 2 }
+ ])
+ await setUserEmailAndSettings(processor.identityDB, 'live', 1)
+
+ await insertNotifications(processor.discoveryDB, [
+ {
+ id: 1,
+ specifier: '2',
+ group_id:
+ 'track_collaborator_invite:track_id:10:collaborator_user_id:1:inviter_user_id:2',
+ type: 'track_collaborator_invite',
+ data: {
+ track_id: 10,
+ collaborator_user_id: 1,
+ inviter_user_id: 2
+ },
+ user_ids: [1]
+ }
+ ])
+
+ await insertMobileSettings(processor.identityDB, [{ userId: 1 }])
+ await insertMobileDevices(processor.identityDB, [{ userId: 1 }])
+
+ const pending = processor.listener.takePending()
+ expect(pending?.appNotifications).toHaveLength(1)
+
+ const title = 'Track Collaboration Invite'
+ const body = 'user_2 invited you to collaborate on track_title_10.'
+ await processor.appNotificationsProcessor.process(pending.appNotifications)
+
+ expect(sendPushNotificationSpy).toHaveBeenCalledWith(
+ {
+ type: 'ios',
+ targetARN: 'arn:1',
+ badgeCount: 1
+ },
+ expect.objectContaining({
+ title,
+ body,
+ data: expect.objectContaining({
+ type: 'TrackCollaboratorInvite',
+ entityId: 10
+ })
+ })
+ )
+
+ expect(sendBrowserNotificationSpy).toHaveBeenCalledWith(
+ true,
+ expect.any(Object),
+ 1,
+ title,
+ body
+ )
+ })
+})
diff --git a/apps/notifications/src/email/notifications/components/Body.tsx b/apps/notifications/src/email/notifications/components/Body.tsx
index 75feb2e..d9e44d8 100644
--- a/apps/notifications/src/email/notifications/components/Body.tsx
+++ b/apps/notifications/src/email/notifications/components/Body.tsx
@@ -124,6 +124,14 @@ const snippetMap = {
const { users } = notification
return `${users[0].name} has been added as a manager on your account.`
},
+ ['track_collaborator_invite'](notification) {
+ const { users } = notification
+ return `${users[0].name} invited you to collaborate on a track.`
+ },
+ ['track_collaborator_accept'](notification) {
+ const { users } = notification
+ return `${users[0].name} accepted your invitation to collaborate.`
+ },
['create'](notification) {
const [user] = notification.users
if (
diff --git a/apps/notifications/src/email/notifications/components/notifications/Notification.tsx b/apps/notifications/src/email/notifications/components/notifications/Notification.tsx
index f3452c2..2d438c5 100644
--- a/apps/notifications/src/email/notifications/components/notifications/Notification.tsx
+++ b/apps/notifications/src/email/notifications/components/notifications/Notification.tsx
@@ -332,6 +332,26 @@ const notificationMap = {
)
},
+ ['track_collaborator_invite'](notification) {
+ const [user] = notification.users
+ return (
+
+
+
+ )
+ },
+ ['track_collaborator_accept'](notification) {
+ const [user] = notification.users
+ return (
+
+
+
+ )
+ },
['create'](notification) {
const [user] = notification.users
if (
diff --git a/apps/notifications/src/processNotifications/mappers/mapNotifications.ts b/apps/notifications/src/processNotifications/mappers/mapNotifications.ts
index 4974730..c3ad626 100644
--- a/apps/notifications/src/processNotifications/mappers/mapNotifications.ts
+++ b/apps/notifications/src/processNotifications/mappers/mapNotifications.ts
@@ -35,6 +35,8 @@ import {
TrackAddedToPurchasedAlbumNotification,
RequestManagerNotification,
ApproveManagerNotification,
+ TrackCollaboratorInviteNotification,
+ TrackCollaboratorAcceptNotification,
ClaimableRewardNotification,
RewardInCooldownNotification,
CommentNotification,
@@ -81,6 +83,8 @@ import { USDCPurchaseBuyer } from './usdcPurchaseBuyer'
import { USDCWithdrawal } from './usdcWithdrawal'
import { USDCTransfer } from './usdcTransfer'
import { RequestManager } from './requestManager'
+import { TrackCollaboratorInvite } from './trackCollaboratorInvite'
+import { TrackCollaboratorAccept } from './trackCollaboratorAccept'
import { TrackAddedToPurchasedAlbum } from './trackAddedToPurchasedAlbum'
import { RewardInCooldown } from './rewardInCooldown'
import { ClaimableReward } from './claimableReward'
@@ -283,6 +287,24 @@ const mapNotification = (
identityDb,
approveManagerNotification
)
+ } else if (notification.type === 'track_collaborator_invite') {
+ const trackCollaboratorInviteNotification = notification as NotificationRow & {
+ data: TrackCollaboratorInviteNotification
+ }
+ return new TrackCollaboratorInvite(
+ dnDb,
+ identityDb,
+ trackCollaboratorInviteNotification
+ )
+ } else if (notification.type === 'track_collaborator_accept') {
+ const trackCollaboratorAcceptNotification = notification as NotificationRow & {
+ data: TrackCollaboratorAcceptNotification
+ }
+ return new TrackCollaboratorAccept(
+ dnDb,
+ identityDb,
+ trackCollaboratorAcceptNotification
+ )
} else if (notification.type === 'claimable_reward') {
const challengeCooldownCompleteNotification =
notification as NotificationRow & {
diff --git a/apps/notifications/src/processNotifications/mappers/trackCollaboratorAccept.ts b/apps/notifications/src/processNotifications/mappers/trackCollaboratorAccept.ts
new file mode 100644
index 0000000..18151a8
--- /dev/null
+++ b/apps/notifications/src/processNotifications/mappers/trackCollaboratorAccept.ts
@@ -0,0 +1,126 @@
+import { Knex } from 'knex'
+import { EntityType } from '../../email/notifications/types'
+import { ResourceIds, Resources } from '../../email/notifications/renderEmail'
+import { sendPushNotification } from '../../sns'
+import { NotificationRow } from '../../types/dn'
+import { TrackCollaboratorAcceptNotification } from '../../types/notifications'
+import { disableDeviceArns } from '../../utils/disableArnEndpoint'
+import { sendBrowserNotification } from '../../web'
+import { BaseNotification } from './base'
+import {
+ Device,
+ buildUserNotificationSettings
+} from './userNotificationSettings'
+
+type TrackCollaboratorAcceptRow = Omit & {
+ data: TrackCollaboratorAcceptNotification
+}
+
+const body = (collaboratorName: string, trackTitle: string): string =>
+ `${collaboratorName} accepted your invitation to collaborate on ${trackTitle}.`
+
+export class TrackCollaboratorAccept extends BaseNotification {
+ trackId: number
+ collaboratorUserId: number
+ inviterUserId: number
+
+ constructor(
+ dnDB: Knex,
+ identityDB: Knex,
+ notification: TrackCollaboratorAcceptRow
+ ) {
+ super(dnDB, identityDB, notification)
+ this.trackId = this.notification.data.track_id
+ this.collaboratorUserId = this.notification.data.collaborator_user_id
+ this.inviterUserId = this.notification.data.inviter_user_id
+ }
+
+ async processNotification({
+ isBrowserPushEnabled
+ }: {
+ isBrowserPushEnabled: boolean
+ }) {
+ const users = await this.getUsersBasicInfo([
+ this.collaboratorUserId,
+ this.inviterUserId
+ ])
+ if (
+ users?.[this.collaboratorUserId]?.is_deactivated ||
+ users?.[this.inviterUserId]?.is_deactivated
+ ) {
+ return
+ }
+
+ const tracks = await this.fetchEntities([this.trackId], EntityType.Track)
+ const track = tracks?.[this.trackId]
+ const trackTitle = track && 'title' in track ? track.title : 'a track'
+ const collaboratorName = users[this.collaboratorUserId].name
+
+ // Notify the inviter (track owner)
+ const userNotificationSettings = await buildUserNotificationSettings(
+ this.identityDB,
+ [this.inviterUserId]
+ )
+
+ const title = 'Collaboration Accepted'
+ const notificationBody = body(collaboratorName, trackTitle)
+
+ await sendBrowserNotification(
+ isBrowserPushEnabled,
+ userNotificationSettings,
+ this.inviterUserId,
+ title,
+ notificationBody
+ )
+
+ if (
+ userNotificationSettings.shouldSendPushNotification({
+ receiverUserId: this.inviterUserId,
+ initiatorUserId: this.collaboratorUserId
+ })
+ ) {
+ const devices: Device[] = userNotificationSettings.getDevices(
+ this.inviterUserId
+ )
+ const pushes = await Promise.all(
+ devices.map((device) => {
+ return sendPushNotification(
+ {
+ type: device.type,
+ badgeCount:
+ userNotificationSettings.getBadgeCount(this.inviterUserId) + 1,
+ targetARN: device.awsARN
+ },
+ {
+ title,
+ body: notificationBody,
+ data: {
+ id: `timestamp:${this.getNotificationTimestamp()}:group_id:${
+ this.notification.group_id
+ }`,
+ type: 'TrackCollaboratorAccept',
+ entityId: this.trackId
+ }
+ }
+ )
+ })
+ )
+ await disableDeviceArns(this.identityDB, pushes)
+ await this.incrementBadgeCount(this.inviterUserId)
+ }
+ }
+
+ getResourcesForEmail(): ResourceIds {
+ return {
+ users: new Set([this.collaboratorUserId])
+ }
+ }
+
+ formatEmailProps(resources: Resources) {
+ const user = resources.users[this.collaboratorUserId]
+ return {
+ type: this.notification.type,
+ users: [user]
+ }
+ }
+}
diff --git a/apps/notifications/src/processNotifications/mappers/trackCollaboratorInvite.ts b/apps/notifications/src/processNotifications/mappers/trackCollaboratorInvite.ts
new file mode 100644
index 0000000..ae71c44
--- /dev/null
+++ b/apps/notifications/src/processNotifications/mappers/trackCollaboratorInvite.ts
@@ -0,0 +1,128 @@
+import { Knex } from 'knex'
+import { EntityType } from '../../email/notifications/types'
+import { ResourceIds, Resources } from '../../email/notifications/renderEmail'
+import { sendPushNotification } from '../../sns'
+import { NotificationRow } from '../../types/dn'
+import { TrackCollaboratorInviteNotification } from '../../types/notifications'
+import { disableDeviceArns } from '../../utils/disableArnEndpoint'
+import { sendBrowserNotification } from '../../web'
+import { BaseNotification } from './base'
+import {
+ Device,
+ buildUserNotificationSettings
+} from './userNotificationSettings'
+
+type TrackCollaboratorInviteRow = Omit & {
+ data: TrackCollaboratorInviteNotification
+}
+
+const body = (inviterName: string, trackTitle: string): string =>
+ `${inviterName} invited you to collaborate on ${trackTitle}.`
+
+export class TrackCollaboratorInvite extends BaseNotification {
+ trackId: number
+ collaboratorUserId: number
+ inviterUserId: number
+
+ constructor(
+ dnDB: Knex,
+ identityDB: Knex,
+ notification: TrackCollaboratorInviteRow
+ ) {
+ super(dnDB, identityDB, notification)
+ this.trackId = this.notification.data.track_id
+ this.collaboratorUserId = this.notification.data.collaborator_user_id
+ this.inviterUserId = this.notification.data.inviter_user_id
+ }
+
+ async processNotification({
+ isBrowserPushEnabled
+ }: {
+ isBrowserPushEnabled: boolean
+ }) {
+ const users = await this.getUsersBasicInfo([
+ this.collaboratorUserId,
+ this.inviterUserId
+ ])
+ if (
+ users?.[this.collaboratorUserId]?.is_deactivated ||
+ users?.[this.inviterUserId]?.is_deactivated
+ ) {
+ return
+ }
+
+ const tracks = await this.fetchEntities([this.trackId], EntityType.Track)
+ const track = tracks?.[this.trackId]
+ const trackTitle = track && 'title' in track ? track.title : 'a track'
+ const inviterName = users[this.inviterUserId].name
+
+ // Notify the invited collaborator
+ const userNotificationSettings = await buildUserNotificationSettings(
+ this.identityDB,
+ [this.collaboratorUserId]
+ )
+
+ const title = 'Track Collaboration Invite'
+ const notificationBody = body(inviterName, trackTitle)
+
+ await sendBrowserNotification(
+ isBrowserPushEnabled,
+ userNotificationSettings,
+ this.collaboratorUserId,
+ title,
+ notificationBody
+ )
+
+ if (
+ userNotificationSettings.shouldSendPushNotification({
+ receiverUserId: this.collaboratorUserId,
+ initiatorUserId: this.inviterUserId
+ })
+ ) {
+ const devices: Device[] = userNotificationSettings.getDevices(
+ this.collaboratorUserId
+ )
+ const pushes = await Promise.all(
+ devices.map((device) => {
+ return sendPushNotification(
+ {
+ type: device.type,
+ badgeCount:
+ userNotificationSettings.getBadgeCount(
+ this.collaboratorUserId
+ ) + 1,
+ targetARN: device.awsARN
+ },
+ {
+ title,
+ body: notificationBody,
+ data: {
+ id: `timestamp:${this.getNotificationTimestamp()}:group_id:${
+ this.notification.group_id
+ }`,
+ type: 'TrackCollaboratorInvite',
+ entityId: this.trackId
+ }
+ }
+ )
+ })
+ )
+ await disableDeviceArns(this.identityDB, pushes)
+ await this.incrementBadgeCount(this.collaboratorUserId)
+ }
+ }
+
+ getResourcesForEmail(): ResourceIds {
+ return {
+ users: new Set([this.inviterUserId])
+ }
+ }
+
+ formatEmailProps(resources: Resources) {
+ const user = resources.users[this.inviterUserId]
+ return {
+ type: this.notification.type,
+ users: [user]
+ }
+ }
+}
diff --git a/apps/notifications/src/types/notifications.ts b/apps/notifications/src/types/notifications.ts
index a7d86ac..44efb87 100644
--- a/apps/notifications/src/types/notifications.ts
+++ b/apps/notifications/src/types/notifications.ts
@@ -157,6 +157,18 @@ export type ApproveManagerNotification = {
user_id: number
}
+export type TrackCollaboratorInviteNotification = {
+ track_id: number
+ collaborator_user_id: number
+ inviter_user_id: number
+}
+
+export type TrackCollaboratorAcceptNotification = {
+ track_id: number
+ collaborator_user_id: number
+ inviter_user_id: number
+}
+
export type RewardInCooldownNotification = {
amount: number
specifier: string