PushNotificationManager.swift 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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 apnsKey: String
  20. private var teamId: String
  21. private var keyId: String
  22. private var user: String
  23. private var bundleId: String
  24. private var cachedJWT: String?
  25. private var jwtExpirationDate: Date?
  26. init() {
  27. self.deviceToken = Storage.shared.deviceToken.value
  28. self.sharedSecret = Storage.shared.sharedSecret.value
  29. self.productionEnvironment = Storage.shared.productionEnvironment.value
  30. self.apnsKey = Storage.shared.apnsKey.value
  31. self.teamId = Storage.shared.teamId.value
  32. self.keyId = Storage.shared.keyId.value
  33. self.user = Storage.shared.user.value
  34. self.bundleId = Storage.shared.bundleId.value
  35. }
  36. func sendBolusPushNotification(commandType: String, bolusAmount: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
  37. let bolusAmount = Decimal(bolusAmount.doubleValue(for: .internationalUnit()))
  38. let message = PushMessage(
  39. user: user,
  40. commandType: commandType,
  41. bolusAmount: bolusAmount,
  42. sharedSecret: sharedSecret,
  43. timestamp: Date().timeIntervalSince1970
  44. )
  45. sendPushNotification(message: message, completion: completion)
  46. }
  47. func sendTempTargetPushNotification(target: HKQuantity, duration: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
  48. let targetValue = Int(target.doubleValue(for: HKUnit.milligramsPerDeciliter))
  49. let durationValue = Int(duration.doubleValue(for: HKUnit.minute()))
  50. let message = PushMessage(
  51. user: user,
  52. commandType: "temp_target",
  53. bolusAmount: nil,
  54. target: targetValue,
  55. duration: durationValue,
  56. sharedSecret: sharedSecret,
  57. timestamp: Date().timeIntervalSince1970
  58. )
  59. sendPushNotification(message: message, completion: completion)
  60. }
  61. func sendCancelTempTargetPushNotification(completion: @escaping (Bool, String?) -> Void) {
  62. let message = PushMessage(
  63. user: user,
  64. commandType: "cancel_temp_target",
  65. sharedSecret: sharedSecret,
  66. timestamp: Date().timeIntervalSince1970
  67. )
  68. sendPushNotification(message: message, completion: completion)
  69. }
  70. func sendMealPushNotification(carbs: HKQuantity, protein: HKQuantity, fat: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
  71. let carbsValue = Int(carbs.doubleValue(for: .gram()))
  72. let proteinValue = Int(protein.doubleValue(for: .gram()))
  73. let fatValue = Int(fat.doubleValue(for: .gram()))
  74. let message = PushMessage(
  75. user: user,
  76. commandType: "meal",
  77. carbs: carbsValue,
  78. protein: proteinValue,
  79. fat: fatValue,
  80. sharedSecret: sharedSecret,
  81. timestamp: Date().timeIntervalSince1970
  82. )
  83. sendPushNotification(message: message, completion: completion)
  84. }
  85. private func sendPushNotification(message: PushMessage, completion: @escaping (Bool, String?) -> Void) {
  86. print("Push message to send: \(message)")
  87. var missingFields = [String]()
  88. if sharedSecret.isEmpty { missingFields.append("sharedSecret") }
  89. if apnsKey.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 !missingFields.isEmpty {
  94. let errorMessage = "Missing required fields, check your remote settings: \(missingFields.joined(separator: ", "))"
  95. print(errorMessage)
  96. completion(false, errorMessage)
  97. return
  98. }
  99. if deviceToken.isEmpty { missingFields.append("deviceToken") }
  100. if bundleId.isEmpty { missingFields.append("bundleId") }
  101. if !missingFields.isEmpty {
  102. let errorMessage = "Missing required data, verify that you are using the latest version of Trio: \(missingFields.joined(separator: ", "))"
  103. print(errorMessage)
  104. completion(false, errorMessage)
  105. return
  106. }
  107. guard let url = constructAPNsURL() else {
  108. let errorMessage = "Failed to construct APNs URL"
  109. print(errorMessage)
  110. completion(false, errorMessage)
  111. return
  112. }
  113. guard let jwt = getOrGenerateJWT() else {
  114. let errorMessage = "Failed to generate JWT, please check that the token is correct."
  115. print(errorMessage)
  116. completion(false, errorMessage)
  117. return
  118. }
  119. var request = URLRequest(url: url)
  120. request.httpMethod = "POST"
  121. request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization")
  122. request.setValue("application/json", forHTTPHeaderField: "content-type")
  123. request.setValue("10", forHTTPHeaderField: "apns-priority")
  124. request.setValue("0", forHTTPHeaderField: "apns-expiration")
  125. request.setValue(bundleId, forHTTPHeaderField: "apns-topic")
  126. request.setValue("background", forHTTPHeaderField: "apns-push-type")
  127. do {
  128. let jsonData = try JSONEncoder().encode(message)
  129. request.httpBody = jsonData
  130. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  131. if let error = error {
  132. let errorMessage = "Failed to send push notification: \(error.localizedDescription)"
  133. print(errorMessage)
  134. completion(false, errorMessage)
  135. return
  136. }
  137. if let httpResponse = response as? HTTPURLResponse {
  138. print("Push notification sent.")
  139. print("Status code: \(httpResponse.statusCode)")
  140. print("Response headers:")
  141. for (key, value) in httpResponse.allHeaderFields {
  142. print("\(key): \(value)")
  143. }
  144. var responseBodyMessage = ""
  145. if let data = data, let responseBody = String(data: data, encoding: .utf8) {
  146. print("Response body: \(responseBody)")
  147. if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
  148. let reason = json["reason"] as? String {
  149. responseBodyMessage = reason
  150. }
  151. } else {
  152. print("No response body")
  153. }
  154. switch httpResponse.statusCode {
  155. case 200:
  156. completion(true, nil)
  157. case 400:
  158. completion(false, "Bad request. The request was invalid or malformed. \(responseBodyMessage)")
  159. case 403:
  160. completion(false, "Authentication error. Check your certificate or authentication token. \(responseBodyMessage)")
  161. case 404:
  162. completion(false, "Invalid request: The :path value was incorrect. \(responseBodyMessage)")
  163. case 405:
  164. completion(false, "Invalid request: Only POST requests are supported. \(responseBodyMessage)")
  165. case 410:
  166. completion(false, "The device token is no longer active for the topic. \(responseBodyMessage)")
  167. case 413:
  168. completion(false, "Payload too large. The notification payload exceeded the size limit. \(responseBodyMessage)")
  169. case 429:
  170. completion(false, "Too many requests. \(responseBodyMessage)")
  171. case 500:
  172. completion(false, "Internal server error at APNs. \(responseBodyMessage)")
  173. case 503:
  174. completion(false, "Service unavailable. The server is temporarily unavailable. Try again later. \(responseBodyMessage)")
  175. default:
  176. completion(false, "Unexpected status code: \(httpResponse.statusCode). \(responseBodyMessage)")
  177. }
  178. } else {
  179. completion(false, "Failed to get a valid HTTP response.")
  180. }
  181. }
  182. task.resume()
  183. } catch {
  184. let errorMessage = "Failed to encode push message: \(error.localizedDescription)"
  185. print(errorMessage)
  186. completion(false, errorMessage)
  187. }
  188. }
  189. private func constructAPNsURL() -> URL? {
  190. let host = productionEnvironment ? "api.push.apple.com" : "api.sandbox.push.apple.com"
  191. let urlString = "https://\(host)/3/device/\(deviceToken)"
  192. return URL(string: urlString)
  193. }
  194. private func getOrGenerateJWT() -> String? {
  195. if let cachedJWT = cachedJWT, let expirationDate = jwtExpirationDate {
  196. if Date() < expirationDate {
  197. return cachedJWT
  198. }
  199. }
  200. let header = Header(kid: keyId)
  201. let claims = APNsJWTClaims(iss: teamId, iat: Date())
  202. var jwt = JWT(header: header, claims: claims)
  203. do {
  204. let privateKey = Data(apnsKey.utf8)
  205. let jwtSigner = JWTSigner.es256(privateKey: privateKey)
  206. let signedJWT = try jwt.sign(using: jwtSigner)
  207. cachedJWT = signedJWT
  208. jwtExpirationDate = Date().addingTimeInterval(3600)
  209. return signedJWT
  210. } catch {
  211. print("Failed to sign JWT: \(error.localizedDescription)")
  212. return nil
  213. }
  214. }
  215. }