Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"date-fns": "^2.29.2",
"dotenv": "^16.0.1",
"ejs": "^3.1.8",
"expo-server-sdk": "^3.11.0",
"express": "^4.18.1",
"generate-password": "^1.7.0",
"graphql": "^16.5.0",
Expand Down
4 changes: 4 additions & 0 deletions src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ const userSchema = new Schema(
required: false
},

pushNotificationTokens: {
type: [String],
default: [],
},
},

{
Expand Down
59 changes: 51 additions & 8 deletions src/resolvers/userResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import { Octokit } from '@octokit/rest'
import bcrypt from 'bcryptjs'
import { GraphQLError } from 'graphql'
// import * as jwt from 'jsonwebtoken'
import { JwtPayload, verify } from 'jsonwebtoken'
import Expo from 'expo-server-sdk'
import jwt, { JwtPayload, verify } from 'jsonwebtoken'
import mongoose, { Error } from 'mongoose'
import generateRandomPassword from '../helpers/generateRandomPassword'
import isAssigned from '../helpers/isAssignedToProgramOrCohort'
import { checkloginAttepmts } from '../helpers/logintracker'
import { checkLoggedInOrganization } from '../helpers/organization.helper'
import {
checkUserLoggedIn,
Expand All @@ -26,19 +25,17 @@ import Program from '../models/program.model'
import { Rating } from '../models/ratings'
import Team from '../models/team.model'
import { RoleOfUser, User, UserRole } from '../models/user'
import { encodeOtpToToken, generateOtp } from '../utils/2WayAuthentication'
import { pushNotification } from '../utils/notification/pushNotification'
import { sendEmail } from '../utils/sendEmail'
import forgotPasswordTemplate from '../utils/templates/forgotPasswordTemplate'
import nonTraineeTemplate from '../utils/templates/nonTraineeTemplate'
import organizationApprovedTemplate from '../utils/templates/organizationApprovedTemplate'
import organizationCreatedTemplate from '../utils/templates/organizationCreatedTemplate'
import organizationRejectedTemplate from '../utils/templates/organizationRejectedTemplate'
import registrationRequest from '../utils/templates/registrationRequestTemplate'
import { EmailPattern } from '../utils/validation.utils'
import { Context } from './../context'
import { UserInputError } from 'apollo-server'
import { encodeOtpToToken, generateOtp } from '../utils/2WayAuthentication'
import jwt from 'jsonwebtoken'
import nonTraineeTemplate from '../utils/templates/nonTraineeTemplate'
const octokit = new Octokit({ auth: `${process.env.Org_Repo_Access}` })

const SECRET = (process.env.SECRET as string) || 'mysq_unique_secret'
Expand Down Expand Up @@ -1110,10 +1107,56 @@ const resolvers: any = {
}
user.pushNotifications = !user.pushNotifications
await user.save()
const updatedPushNotifications = user.pushNotifications
return 'updated successful'
},

async addPushNotificationToken(
_: any,
{ pushToken }: { pushToken: string },
context: Context
) {
if (!Expo.isExpoPushToken(pushToken)) {
throw new Error('Invalid push notification')
}

const user: any = await User.findOne({ _id: context.userId })
if (!user) {
throw new Error('User not found')
}

if (!user.pushNotificationTokens.includes(pushToken)) {
user.pushNotificationTokens.push(pushToken)
await user.save()
return 'Notification token added successfully'
}

return 'Notification token already added'
},

async removePushNotificationToken(
_: any,
{ pushToken }: { pushToken: string },
context: Context
) {
if (!Expo.isExpoPushToken(pushToken)) {
throw new Error('Invalid push notification')
}

const user: any = await User.findOne({ _id: context.userId })
if (!user) {
throw new Error('User not found')
}

const index = user.pushNotificationTokens.indexOf(pushToken)
if (index !== -1) {
user.pushNotificationTokens.splice(index, 1)
await user.save()
return 'Notification token removed successfully'
}

return 'Notification token not found'
},

async resetUserPassword(
_: any,
{ password, confirmPassword, token }: any,
Expand Down
3 changes: 3 additions & 0 deletions src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const Schema = gql`
TwoWayVerificationToken: String
createdAt: String
updatedAt: String
pushNotificationTokens: [String]
}
input RegisterInput {
email: String!
Expand Down Expand Up @@ -586,6 +587,8 @@ const Schema = gql`
type Mutation {
updatePushNotifications(id: ID!): String
updateEmailNotifications(id: ID!): String
addPushNotificationToken(pushToken: String): String
removePushNotificationToken(pushToken: String): String
}
type Query {
getUpdatedEmailNotifications(id: ID!): Boolean!
Expand Down
132 changes: 128 additions & 4 deletions src/utils/notification/pushNotification.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import {
Expo,
ExpoPushErrorReceipt,
ExpoPushMessage,
ExpoPushReceipt,
ExpoPushTicket,
} from 'expo-server-sdk'
import mongoose from 'mongoose'
import { Notification } from '../../models/notification.model'
import { pubSubPublish } from '../../resolvers/notification.resolvers'
import { User } from '../../models/user'
import { Profile } from '../../models/profile.model'
import { User } from '../../models/user'
import { pubSubPublish } from '../../resolvers/notification.resolvers'

export type NotificationData = {
redirectURI?: string
criteria: {
type: 'PUBLIC' | 'PERSONAL' | 'TEAM' | 'ORGANIZATION'
value: string
}
}

let expo = new Expo({
accessToken: process.env.EXPO_ACCESS_TOKEN,
})

export const pushNotification = async (
receiver: mongoose.Types.ObjectId,
Expand All @@ -29,8 +48,113 @@ export const pushNotification = async (
id: notification.id,
sender: { profile: profile?.toObject() },
}
const userExists = await User.findOne({ _id: receiver })
if (userExists && userExists.pushNotifications) {
const receivingUser = await User.findOne({ _id: receiver })
if (receivingUser && receivingUser.pushNotifications) {
pubSubPublish(sanitizedNotification)

if (receivingUser.pushNotificationTokens.length > 0) {
const notificationData: NotificationData = {
redirectURI: `/dashboard/trainee${
notification.type ? '/' + notification.type : ''
}`,
criteria: { type: 'PERSONAL', value: receivingUser.id },
}

console.log('Sending push notifications')

sendPushNotifications(
receivingUser.pushNotificationTokens,
`${capitalizeString(notification.type)} notification`,
notification.message,
notificationData
)
}
}
}

const capitalizeString = (str?: string) => {
str = str || 'New'
return str.charAt(0).toUpperCase() + str.slice(1)
}

const sendPushNotifications = async (
pushTokens: string[],
title: string,
body: string,
data: NotificationData
) => {
// Create the messages that you want to send to clients
let messages: ExpoPushMessage[] = []
for (let pushToken of pushTokens) {
// Each push token looks like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
// Check that all your push tokens appear to be valid Expo push tokens
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`)
continue
}

messages.push({
to: pushToken,
title: title,
body: body,
data: data,
})
}

// The Expo push notification service accepts batches of notifications so
// that you don't need to send 1000 requests to send 1000 notifications.
let chunks = expo.chunkPushNotifications(messages)
let tickets: ExpoPushTicket[] = []
;(async () => {
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk)
console.log(ticketChunk)
tickets.push(...ticketChunk)
} catch (error) {
console.error(error)
}
}
})()

let receiptIds = []
for (let ticket of tickets) {
// NOTE: Not all tickets have IDs; for example, tickets for notifications
// that could not be enqueued will have error information and no receipt ID.
if (ticket.status === 'ok') {
receiptIds.push(ticket.id)
}

if (ticket.status === 'error' && ticket.details) {
console.error(ticket.details.error)
}
}

let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds)
;(async () => {
// Like sending notifications, there are different strategies you could use
// to retrieve batches of receipts from the Expo service.
for (let chunk of receiptIdChunks) {
try {
let receipts = await expo.getPushNotificationReceiptsAsync(chunk)
console.log(receipts)

// The receipts specify whether Apple or Google successfully received the
// notification and information about an error, if one occurred.
for (let receiptId in receipts) {
let { status, details }: ExpoPushReceipt | ExpoPushErrorReceipt =
receipts[receiptId]
if (status === 'ok') {
continue
} else if (status === 'error') {
console.error(
`There was an error sending a notification: ${details}`
)
}
}
} catch (error) {
console.error(error)
}
}
})()
}
Loading