@@ -28,16 +28,23 @@ class RemoteSettingsViewModel: ObservableObject {
2828 @Published var loopAPNSQrCodeURL : String
2929 @Published var loopAPNSDeviceToken : String
3030 @Published var loopAPNSBundleIdentifier : String
31- @Published var loopAPNSSetup : Bool
3231 @Published var productionEnvironment : Bool
3332 @Published var isShowingLoopAPNSScanner : Bool = false
3433 @Published var loopAPNSErrorMessage : String ?
3534 @Published var isRefreshingDeviceToken : Bool = false
3635
36+ // MARK: - Computed property for Loop APNS Setup validation
37+
38+ var loopAPNSSetup : Bool {
39+ !keyId. isEmpty &&
40+ !apnsKey. isEmpty &&
41+ !loopAPNSQrCodeURL. isEmpty &&
42+ !loopAPNSDeviceToken. isEmpty &&
43+ !loopAPNSBundleIdentifier. isEmpty
44+ }
45+
3746 private var storage = Storage . shared
3847 private var cancellables = Set < AnyCancellable > ( )
39- private var isUpdatingLoopAPNSSetup = false
40- private var lastValidationTime : Date = . distantPast
4148
4249 init ( ) {
4350 // Initialize published properties from storage
@@ -57,16 +64,13 @@ class RemoteSettingsViewModel: ObservableObject {
5764 loopAPNSQrCodeURL = storage. loopAPNSQrCodeURL. value
5865 loopAPNSDeviceToken = storage. loopAPNSDeviceToken. value
5966 loopAPNSBundleIdentifier = storage. loopAPNSBundleIdentifier. value
60- loopAPNSSetup = storage. loopAPNSSetup. value
6167 productionEnvironment = storage. productionEnvironment. value
6268
6369 setupBindings ( )
64-
65- // Trigger initial validation
66- validateFullLoopAPNSSetup ( )
6770 }
6871
6972 private func setupBindings( ) {
73+ // Basic property bindings
7074 $remoteType
7175 . dropFirst ( )
7276 . sink { [ weak self] in self ? . storage. remoteType. value = $0 }
@@ -84,7 +88,12 @@ class RemoteSettingsViewModel: ObservableObject {
8488
8589 $apnsKey
8690 . dropFirst ( )
87- . sink { [ weak self] in self ? . storage. apnsKey. value = $0 }
91+ . sink { [ weak self] newValue in
92+ // Validate and fix the APNS key format using the service
93+ let apnsService = LoopAPNSService ( )
94+ let fixedKey = apnsService. validateAndFixAPNSKey ( newValue)
95+ self ? . storage. apnsKey. value = fixedKey
96+ }
8897 . store ( in: & cancellables)
8998
9099 $keyId
@@ -122,6 +131,7 @@ class RemoteSettingsViewModel: ObservableObject {
122131 . sink { [ weak self] in self ? . storage. mealWithFatProtein. value = $0 }
123132 . store ( in: & cancellables)
124133
134+ // Device type monitoring
125135 Storage . shared. device. $value
126136 . receive ( on: DispatchQueue . main)
127137 . sink { [ weak self] newValue in
@@ -130,29 +140,7 @@ class RemoteSettingsViewModel: ObservableObject {
130140 }
131141 . store ( in: & cancellables)
132142
133- // Loop APNS setup bindings
134- $keyId
135- . dropFirst ( )
136- . sink { [ weak self] in self ? . storage. keyId. value = $0 }
137- . store ( in: & cancellables)
138-
139- $apnsKey
140- . dropFirst ( )
141- . sink { [ weak self] newValue in
142- // Log APNS key changes for debugging
143- LogManager . shared. log ( category: . apns, message: " APNS Key changed - Length: \( newValue. count) " )
144- LogManager . shared. log ( category: . apns, message: " APNS Key contains line breaks: \( newValue. contains ( " \n " ) ) " )
145- LogManager . shared. log ( category: . apns, message: " APNS Key contains BEGIN header: \( newValue. contains ( " -----BEGIN PRIVATE KEY----- " ) ) " )
146- LogManager . shared. log ( category: . apns, message: " APNS Key contains END header: \( newValue. contains ( " -----END PRIVATE KEY----- " ) ) " )
147-
148- // Validate and fix the APNS key format using the service
149- let apnsService = LoopAPNSService ( )
150- let fixedKey = apnsService. validateAndFixAPNSKey ( newValue)
151-
152- self ? . storage. apnsKey. value = fixedKey
153- }
154- . store ( in: & cancellables)
155-
143+ // Loop APNS bindings
156144 $loopDeveloperTeamId
157145 . dropFirst ( )
158146 . sink { [ weak self] in self ? . storage. teamId. value = $0 }
@@ -173,99 +161,14 @@ class RemoteSettingsViewModel: ObservableObject {
173161 . sink { [ weak self] in self ? . storage. loopAPNSBundleIdentifier. value = $0 }
174162 . store ( in: & cancellables)
175163
176- // Sync loopAPNSSetup with storage
177- Storage . shared. loopAPNSSetup. $value
178- . receive ( on: DispatchQueue . main)
179- . sink { [ weak self] newValue in
180- guard let self = self , !self . isUpdatingLoopAPNSSetup else { return }
181- self . loopAPNSSetup = newValue
182- }
183- . store ( in: & cancellables)
184-
185164 $productionEnvironment
186165 . dropFirst ( )
187166 . sink { [ weak self] in self ? . storage. productionEnvironment. value = $0 }
188167 . store ( in: & cancellables)
189-
190- // Auto-validate Loop APNS setup when key ID, APNS key, or QR code changes
191- Publishers . CombineLatest3 ( $keyId, $apnsKey, $loopAPNSQrCodeURL)
192- . dropFirst ( )
193- . receive ( on: DispatchQueue . main)
194- . sink { [ weak self] _, _, _ in
195- self ? . validateFullLoopAPNSSetup ( )
196- }
197- . store ( in: & cancellables)
198-
199- // Auto-validate when device token or bundle identifier changes
200- Publishers . CombineLatest ( $loopAPNSDeviceToken, $loopAPNSBundleIdentifier)
201- . dropFirst ( )
202- . receive ( on: DispatchQueue . main)
203- . sink { [ weak self] _, _ in
204- self ? . validateFullLoopAPNSSetup ( )
205- }
206- . store ( in: & cancellables)
207-
208- // Auto-validate when production environment changes
209- $productionEnvironment
210- . dropFirst ( )
211- . receive ( on: DispatchQueue . main)
212- . sink { [ weak self] _ in
213- self ? . validateFullLoopAPNSSetup ( )
214- }
215- . store ( in: & cancellables)
216-
217- $loopDeveloperTeamId
218- . dropFirst ( )
219- . receive ( on: DispatchQueue . main)
220- . sink { [ weak self] _ in
221- self ? . validateFullLoopAPNSSetup ( )
222- }
223- . store ( in: & cancellables)
224168 }
225169
226170 // MARK: - Loop APNS Setup Methods
227171
228- /// Validates the full Loop APNS setup including device token and bundle identifier
229- /// - Returns: True if full setup is valid, false otherwise
230- func validateFullLoopAPNSSetup( ) {
231- // Debounce rapid successive calls (prevent calls within 100ms of each other)
232- let now = Date ( )
233- if now. timeIntervalSince ( lastValidationTime) < 0.1 {
234- LogManager . shared. log ( category: . apns, message: " Skipping validation - too soon since last call " )
235- return
236- }
237- lastValidationTime = now
238-
239- // Add call stack debugging
240- let callStack = Thread . callStackSymbols. prefix ( 3 ) . map { $0. components ( separatedBy: " " ) . last ?? " unknown " } . joined ( separator: " -> " )
241- LogManager . shared. log ( category: . apns, message: " validateFullLoopAPNSSetup called from: \( callStack) " )
242-
243- let hasKeyId = !keyId. isEmpty
244- let hasAPNSKey = !apnsKey. isEmpty
245- let hasQrCode = !loopAPNSQrCodeURL. isEmpty
246- let hasDeviceToken = !loopAPNSDeviceToken. isEmpty
247- let hasBundleIdentifier = !loopAPNSBundleIdentifier. isEmpty
248-
249- let hasFullSetup = hasKeyId && hasAPNSKey && hasQrCode && hasDeviceToken && hasBundleIdentifier
250-
251- let oldSetup = loopAPNSSetup
252-
253- // Only update storage if the value has actually changed to prevent infinite loops
254- if storage. loopAPNSSetup. value != hasFullSetup {
255- isUpdatingLoopAPNSSetup = true
256- storage. loopAPNSSetup. value = hasFullSetup
257- isUpdatingLoopAPNSSetup = false
258- }
259-
260- // Log validation results for debugging
261- LogManager . shared. log ( category: . apns, message: " Full Loop APNS setup validation - Key ID: \( hasKeyId) , APNS Key: \( hasAPNSKey) , QR Code: \( hasQrCode) , Device Token: \( hasDeviceToken) , Bundle ID: \( hasBundleIdentifier) , Valid: \( hasFullSetup) " )
262-
263- // Post notification if setup status changed
264- if oldSetup != hasFullSetup {
265- NotificationCenter . default. post ( name: NSNotification . Name ( " LoopAPNSSetupChanged " ) , object: nil )
266- }
267- }
268-
269172 func refreshDeviceToken( ) async {
270173 await MainActor . run {
271174 isRefreshingDeviceToken = true
@@ -280,8 +183,6 @@ class RemoteSettingsViewModel: ObservableObject {
280183 if success {
281184 self . loopAPNSDeviceToken = self . storage. loopAPNSDeviceToken. value
282185 self . loopAPNSBundleIdentifier = self . storage. loopAPNSBundleIdentifier. value
283- // Trigger validation immediately after updating values
284- self . validateFullLoopAPNSSetup ( )
285186 } else {
286187 self . loopAPNSErrorMessage = " Failed to refresh device token. Check your Nightscout URL and token. "
287188 }
@@ -308,13 +209,6 @@ class RemoteSettingsViewModel: ObservableObject {
308209 case let . success( profileData) :
309210 // Log the profile data for debugging
310211 LogManager . shared. log ( category: . apns, message: " Profile fetched successfully for device token " )
311- LogManager . shared. log ( category: . apns, message: " Device token from profile: \( profileData. deviceToken ?? " nil " ) " )
312- LogManager . shared. log ( category: . apns, message: " Bundle identifier from profile: \( profileData. bundleIdentifier ?? " nil " ) " )
313-
314- if let loopSettings = profileData. loopSettings {
315- LogManager . shared. log ( category: . apns, message: " Loop settings device token: \( loopSettings. deviceToken ?? " nil " ) " )
316- LogManager . shared. log ( category: . apns, message: " Loop settings bundle identifier: \( loopSettings. bundleIdentifier ?? " nil " ) " )
317- }
318212
319213 // Update profile data which includes device token and bundle identifier
320214 ProfileManager . shared. loadProfile ( from: profileData)
@@ -332,10 +226,6 @@ class RemoteSettingsViewModel: ObservableObject {
332226 self . storage. loopAPNSBundleIdentifier. value = bundleIdentifier
333227 }
334228
335- // Log the stored values after processing
336- LogManager . shared. log ( category: . apns, message: " Stored device token: \( self . storage. loopAPNSDeviceToken. value) " )
337- LogManager . shared. log ( category: . apns, message: " Stored bundle ID: \( self . storage. loopAPNSBundleIdentifier. value) " )
338-
339229 // Log successful configuration
340230 LogManager . shared. log ( category: . apns, message: " Successfully configured device tokens from Nightscout profile " )
341231
@@ -355,9 +245,7 @@ class RemoteSettingsViewModel: ObservableObject {
355245 switch result {
356246 case let . success( code) :
357247 self . loopAPNSQrCodeURL = code
358- // Trigger validation after QR code is scanned
359248 LogManager . shared. log ( category: . apns, message: " Loop APNS QR code scanned: \( code) " )
360- self . validateFullLoopAPNSSetup ( )
361249 case let . failure( error) :
362250 self . loopAPNSErrorMessage = " Scanning failed: \( error. localizedDescription) "
363251 }
0 commit comments