|
|
@@ -1,6 +1,6 @@
|
|
|
// LoopFollow
|
|
|
// LoopAPNSService.swift
|
|
|
-// Created by codebymini.
|
|
|
+// Created by Daniel Mini Johansson.
|
|
|
|
|
|
import CryptoKit
|
|
|
import Foundation
|
|
|
@@ -48,6 +48,51 @@ class LoopAPNSService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func createReturnNotificationInfo() -> [String: Any]? {
|
|
|
+ let loopFollowDeviceToken = Observable.shared.loopFollowDeviceToken.value
|
|
|
+ guard !loopFollowDeviceToken.isEmpty else { return nil }
|
|
|
+
|
|
|
+ // Get LoopFollow's own Team ID from BuildDetails.
|
|
|
+ guard let loopFollowTeamID = BuildDetails.default.teamID, !loopFollowTeamID.isEmpty else {
|
|
|
+ LogManager.shared.log(category: .apns, message: "LoopFollow Team ID not found in BuildDetails.plist. Cannot create return notification info.")
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get the target Loop app's Team ID from storage.
|
|
|
+ let targetTeamId = storage.teamId.value ?? ""
|
|
|
+ let teamIdsAreDifferent = loopFollowTeamID != targetTeamId
|
|
|
+
|
|
|
+ let keyIdForReturn: String
|
|
|
+ let apnsKeyForReturn: String
|
|
|
+
|
|
|
+ if teamIdsAreDifferent {
|
|
|
+ // Team IDs differ, use the separate return credentials.
|
|
|
+ keyIdForReturn = storage.returnKeyId.value
|
|
|
+ apnsKeyForReturn = storage.returnApnsKey.value
|
|
|
+ } else {
|
|
|
+ // Team IDs are the same, use the primary credentials.
|
|
|
+ keyIdForReturn = storage.keyId.value
|
|
|
+ apnsKeyForReturn = storage.apnsKey.value
|
|
|
+ }
|
|
|
+
|
|
|
+ // Ensure we have the necessary credentials.
|
|
|
+ guard !keyIdForReturn.isEmpty, !apnsKeyForReturn.isEmpty else {
|
|
|
+ LogManager.shared.log(category: .apns, message: "Missing required return APNS credentials. Check Remote Settings.")
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ let returnInfo: [String: Any] = [
|
|
|
+ "production_environment": BuildDetails.default.isTestFlightBuild(),
|
|
|
+ "device_token": loopFollowDeviceToken,
|
|
|
+ "bundle_id": Bundle.main.bundleIdentifier ?? "",
|
|
|
+ "team_id": loopFollowTeamID,
|
|
|
+ "key_id": keyIdForReturn,
|
|
|
+ "apns_key": apnsKeyForReturn,
|
|
|
+ ]
|
|
|
+
|
|
|
+ return returnInfo
|
|
|
+ }
|
|
|
+
|
|
|
/// Validates the Loop APNS setup by checking all required fields
|
|
|
/// - Returns: True if setup is valid, false otherwise
|
|
|
func validateSetup() -> Bool {
|
|
|
@@ -88,7 +133,7 @@ class LoopAPNSService {
|
|
|
let carbsAmount = payload.carbsAmount ?? 0.0
|
|
|
let absorptionTime = payload.absorptionTime ?? 3.0
|
|
|
let startTime = payload.consumedDate ?? now
|
|
|
- let finalPayload = [
|
|
|
+ var finalPayload = [
|
|
|
"carbs-entry": carbsAmount,
|
|
|
"absorption-time": absorptionTime,
|
|
|
"otp": String(payload.otp),
|
|
|
@@ -101,6 +146,10 @@ class LoopAPNSService {
|
|
|
"alert": "Remote Carbs Entry: \(String(format: "%.1f", carbsAmount)) grams\nAbsorption Time: \(String(format: "%.1f", absorptionTime)) hours",
|
|
|
] as [String: Any]
|
|
|
|
|
|
+ if let returnInfo = createReturnNotificationInfo() {
|
|
|
+ finalPayload["return_notification"] = returnInfo
|
|
|
+ }
|
|
|
+
|
|
|
// Log the exact carbs amount for debugging precision issues
|
|
|
LogManager.shared.log(category: .apns, message: "Carbs amount - Raw: \(payload.carbsAmount ?? 0.0), Formatted: \(String(format: "%.1f", carbsAmount)), JSON: \(carbsAmount)")
|
|
|
LogManager.shared.log(category: .apns, message: "Absorption time - Raw: \(payload.absorptionTime ?? 3.0), Formatted: \(String(format: "%.1f", absorptionTime)), JSON: \(absorptionTime)")
|
|
|
@@ -139,7 +188,7 @@ class LoopAPNSService {
|
|
|
// Create the complete notification payload (matching Nightscout's exact format)
|
|
|
// Based on Nightscout's loop.js implementation
|
|
|
let bolusAmount = payload.bolusAmount ?? 0.0
|
|
|
- let finalPayload = [
|
|
|
+ var finalPayload = [
|
|
|
"bolus-entry": bolusAmount,
|
|
|
"otp": String(payload.otp),
|
|
|
"remote-address": "LoopFollow",
|
|
|
@@ -150,6 +199,10 @@ class LoopAPNSService {
|
|
|
"alert": "Remote Bolus Entry: \(String(format: "%.2f", bolusAmount)) U",
|
|
|
] as [String: Any]
|
|
|
|
|
|
+ if let returnInfo = createReturnNotificationInfo() {
|
|
|
+ finalPayload["return_notification"] = returnInfo
|
|
|
+ }
|
|
|
+
|
|
|
// Log the exact bolus amount for debugging precision issues
|
|
|
LogManager.shared.log(category: .apns, message: "Bolus amount - Raw: \(payload.bolusAmount ?? 0.0), Formatted: \(String(format: "%.2f", bolusAmount)), JSON: \(bolusAmount)")
|
|
|
|
|
|
@@ -505,6 +558,10 @@ class LoopAPNSService {
|
|
|
payload["override-duration-minutes"] = Int(duration / 60)
|
|
|
}
|
|
|
|
|
|
+ if let returnInfo = createReturnNotificationInfo() {
|
|
|
+ payload["return_notification"] = returnInfo
|
|
|
+ }
|
|
|
+
|
|
|
// Send the notification using the existing APNS infrastructure
|
|
|
try await sendAPNSNotification(
|
|
|
deviceToken: deviceToken,
|
|
|
@@ -530,7 +587,7 @@ class LoopAPNSService {
|
|
|
let now = Date()
|
|
|
let expiration = Date(timeIntervalSinceNow: 5 * 60) // 5 minutes from now
|
|
|
|
|
|
- let payload: [String: Any] = [
|
|
|
+ var payload: [String: Any] = [
|
|
|
"cancel-temporary-override": "true",
|
|
|
"remote-address": "LoopFollow",
|
|
|
"entered-by": "LoopFollow",
|
|
|
@@ -539,6 +596,10 @@ class LoopAPNSService {
|
|
|
"alert": "Cancel Temporary Override",
|
|
|
]
|
|
|
|
|
|
+ if let returnInfo = createReturnNotificationInfo() {
|
|
|
+ payload["return_notification"] = returnInfo
|
|
|
+ }
|
|
|
+
|
|
|
// Send the notification using the existing APNS infrastructure
|
|
|
try await sendAPNSNotification(
|
|
|
deviceToken: deviceToken,
|