PushNotificationManager.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. //
  2. // PushNotificationManager.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2024-08-27.
  6. // Copyright © 2024 Jon Fawcett. All rights reserved.
  7. //
  8. import Foundation
  9. import SwiftJWT
  10. import HealthKit
  11. struct APNsJWTClaims: Claims {
  12. let iss: String
  13. let iat: Date
  14. }
  15. class PushNotificationManager {
  16. private var deviceToken: String
  17. private var sharedSecret: String
  18. private var productionEnvironment: Bool
  19. private var token: String
  20. private var teamId: String
  21. private var keyId: String
  22. private var user: String
  23. private var bundleId: String
  24. init() {
  25. self.deviceToken = Storage.shared.deviceToken.value
  26. self.sharedSecret = Storage.shared.sharedSecret.value
  27. self.productionEnvironment = Storage.shared.productionEnvironment.value
  28. self.token = Storage.shared.token.value
  29. self.teamId = Storage.shared.teamId.value
  30. self.keyId = Storage.shared.keyId.value
  31. self.user = Storage.shared.user.value
  32. self.bundleId = Storage.shared.bundleId.value
  33. }
  34. func sendBolusPushNotification(commandType: String, bolusAmount: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
  35. let bolusAmount = Decimal(bolusAmount.doubleValue(for: .internationalUnit()))
  36. let message = PushMessage(
  37. user: user,
  38. commandType: commandType,
  39. bolusAmount: bolusAmount,
  40. sharedSecret: sharedSecret,
  41. timestamp: Date().timeIntervalSince1970
  42. )
  43. sendPushNotification(message: message, completion: completion)
  44. }
  45. func sendTempTargetPushNotification(target: HKQuantity, duration: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
  46. let targetValue = Int(target.doubleValue(for: HKUnit.milligramsPerDeciliter))
  47. let durationValue = Int(duration.doubleValue(for: HKUnit.minute()))
  48. let message = PushMessage(
  49. user: user,
  50. commandType: "temp_target",
  51. bolusAmount: nil,
  52. target: targetValue,
  53. duration: durationValue,
  54. sharedSecret: sharedSecret,
  55. timestamp: Date().timeIntervalSince1970
  56. )
  57. sendPushNotification(message: message, completion: completion)
  58. }
  59. func sendCancelTempTargetPushNotification(completion: @escaping (Bool, String?) -> Void) {
  60. let message = PushMessage(
  61. user: user,
  62. commandType: "cancel_temp_target",
  63. sharedSecret: sharedSecret,
  64. timestamp: Date().timeIntervalSince1970
  65. )
  66. sendPushNotification(message: message, completion: completion)
  67. }
  68. func sendMealPushNotification(carbs: HKQuantity, protein: HKQuantity, fat: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
  69. let carbsValue = Int(carbs.doubleValue(for: .gram()))
  70. let proteinValue = Int(protein.doubleValue(for: .gram()))
  71. let fatValue = Int(fat.doubleValue(for: .gram()))
  72. let message = PushMessage(
  73. user: user,
  74. commandType: "meal",
  75. carbs: carbsValue,
  76. protein: proteinValue,
  77. fat: fatValue,
  78. sharedSecret: sharedSecret,
  79. timestamp: Date().timeIntervalSince1970
  80. )
  81. sendPushNotification(message: message, completion: completion)
  82. }
  83. private func sendPushNotification(message: PushMessage, completion: @escaping (Bool, String?) -> Void) {
  84. print("Push message to send: \(message)")
  85. // Validate that all required fields are filled
  86. var missingFields = [String]()
  87. if deviceToken.isEmpty { missingFields.append("deviceToken") }
  88. if sharedSecret.isEmpty { missingFields.append("sharedSecret") }
  89. if token.isEmpty { missingFields.append("token") }
  90. if teamId.isEmpty { missingFields.append("teamId") }
  91. if keyId.isEmpty { missingFields.append("keyId") }
  92. if user.isEmpty { missingFields.append("user") }
  93. if bundleId.isEmpty { missingFields.append("bundleId") }
  94. if !missingFields.isEmpty {
  95. let errorMessage = "Missing required fields: \(missingFields.joined(separator: ", "))"
  96. print(errorMessage)
  97. completion(false, errorMessage)
  98. return
  99. }
  100. guard let url = constructAPNsURL() else {
  101. let errorMessage = "Failed to construct APNs URL"
  102. print(errorMessage)
  103. completion(false, errorMessage)
  104. return
  105. }
  106. guard let jwt = generateJWT() else {
  107. let errorMessage = "Failed to generate JWT, please check that the token is correct."
  108. print(errorMessage)
  109. completion(false, errorMessage)
  110. return
  111. }
  112. var request = URLRequest(url: url)
  113. request.httpMethod = "POST"
  114. request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization")
  115. request.setValue("application/json", forHTTPHeaderField: "content-type")
  116. request.setValue("10", forHTTPHeaderField: "apns-priority")
  117. request.setValue("0", forHTTPHeaderField: "apns-expiration")
  118. request.setValue(bundleId, forHTTPHeaderField: "apns-topic")
  119. request.setValue("background", forHTTPHeaderField: "apns-push-type")
  120. do {
  121. let jsonData = try JSONEncoder().encode(message)
  122. request.httpBody = jsonData
  123. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  124. if let error = error {
  125. let errorMessage = "Failed to send push notification: \(error.localizedDescription)"
  126. print(errorMessage)
  127. completion(false, errorMessage)
  128. return
  129. }
  130. if let httpResponse = response as? HTTPURLResponse {
  131. print("Push notification sent. Status code: \(httpResponse.statusCode)")
  132. switch httpResponse.statusCode {
  133. case 200:
  134. completion(true, nil)
  135. case 400:
  136. completion(false, "Bad request.")
  137. case 403:
  138. completion(false, "Authentication error. Check Token, Team ID and Key ID.")
  139. case 405:
  140. completion(false, "Method not allowed.")
  141. case 413:
  142. completion(false, "Payload too large.")
  143. case 429:
  144. completion(false, "Too many requests. Rate limit exceeded.")
  145. case 500:
  146. completion(false, "Internal server error at APNs.")
  147. case 503:
  148. completion(false, "Service unavailable. Try again later.")
  149. default:
  150. completion(false, "Unexpected status code: \(httpResponse.statusCode)")
  151. }
  152. } else {
  153. completion(false, "Failed to get a valid HTTP response.")
  154. }
  155. }
  156. task.resume()
  157. } catch {
  158. let errorMessage = "Failed to encode push message: \(error.localizedDescription)"
  159. print(errorMessage)
  160. completion(false, errorMessage)
  161. }
  162. }
  163. private func constructAPNsURL() -> URL? {
  164. let host = productionEnvironment ? "api.push.apple.com" : "api.sandbox.push.apple.com"
  165. let urlString = "https://\(host)/3/device/\(deviceToken)"
  166. return URL(string: urlString)
  167. }
  168. private func generateJWT() -> String? {
  169. let header = Header(kid: keyId)
  170. let claims = APNsJWTClaims(iss: teamId, iat: Date())
  171. var jwt = JWT(header: header, claims: claims)
  172. do {
  173. let privateKey = Data(token.utf8)
  174. let jwtSigner = JWTSigner.es256(privateKey: privateKey)
  175. let signedJWT = try jwt.sign(using: jwtSigner)
  176. return signedJWT
  177. } catch {
  178. print("Failed to sign JWT: \(error.localizedDescription)")
  179. return nil
  180. }
  181. }
  182. }