Skip to content

Commit fe90560

Browse files
authored
Merge pull request #450 from CodeByMiniOrg/block-same-totp
Block sending loop commands with same TOTP code
2 parents 784a380 + 07ae3f7 commit fe90560

File tree

5 files changed

+145
-6
lines changed

5 files changed

+145
-6
lines changed

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
1111
654132E72E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */; };
1212
654132EA2E19F24800BDBE08 /* TOTPGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E92E19F24800BDBE08 /* TOTPGenerator.swift */; };
13-
6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
1413
654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */; };
1514
6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134192E1DC27900BDBE08 /* OverridePresetData.swift */; };
15+
6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
16+
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; };
1617
DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; };
1718
DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; };
1819
DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; };
@@ -393,9 +394,10 @@
393394
059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
394395
654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleQRCodeScannerView.swift; sourceTree = "<group>"; };
395396
654132E92E19F24800BDBE08 /* TOTPGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPGenerator.swift; sourceTree = "<group>"; };
396-
6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
397397
654134172E1DC09700BDBE08 /* OverridePresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetsView.swift; sourceTree = "<group>"; };
398398
654134192E1DC27900BDBE08 /* OverridePresetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetData.swift; sourceTree = "<group>"; };
399+
6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
400+
6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = "<group>"; };
399401
A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
400402
DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = "<group>"; };
401403
DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = "<group>"; };
@@ -1191,6 +1193,7 @@
11911193
DDEF503E2D479B8A00884336 /* LoopAPNS */ = {
11921194
isa = PBXGroup;
11931195
children = (
1196+
6584B1002E4A263900135D4D /* TOTPService.swift */,
11941197
DDEF503F2D479B8A00884336 /* LoopAPNSService.swift */,
11951198
DDEF50412D479BAA00884336 /* LoopAPNSCarbsView.swift */,
11961199
DDEF50422D479BBA00884336 /* LoopAPNSBolusView.swift */,
@@ -1936,6 +1939,7 @@
19361939
DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */,
19371940
DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */,
19381941
DD0650F32DCE9B3D004D3B41 /* MissedReadingEditor.swift in Sources */,
1942+
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */,
19391943
DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */,
19401944
DDEF503A2D31615000999A5D /* LogManager.swift in Sources */,
19411945
DD4878172C7B75350048F05C /* BolusView.swift in Sources */,

LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ struct LoopAPNSBolusView: View {
2323
private let otpPeriod: TimeInterval = 30
2424
private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
2525

26+
// Computed property to check if TOTP should be blocked
27+
private var isTOTPBlocked: Bool {
28+
TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
29+
}
30+
2631
enum AlertType {
2732
case success
2833
case error
@@ -115,9 +120,29 @@ struct LoopAPNSBolusView: View {
115120
Text("Send Insulin")
116121
}
117122
}
118-
.disabled(insulinAmount.doubleValue(for: .internationalUnit()) <= 0 || isLoading)
123+
.disabled(insulinAmount.doubleValue(for: .internationalUnit()) <= 0 || isLoading || isTOTPBlocked)
119124
.frame(maxWidth: .infinity)
120125
}
126+
127+
// TOTP Blocking Warning Section
128+
if isTOTPBlocked {
129+
Section {
130+
VStack(alignment: .leading, spacing: 8) {
131+
HStack {
132+
Image(systemName: "exclamationmark.triangle.fill")
133+
.foregroundColor(.orange)
134+
Text("TOTP Code Already Used")
135+
.font(.headline)
136+
.foregroundColor(.orange)
137+
}
138+
Text("This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command.")
139+
.font(.caption)
140+
.foregroundColor(.secondary)
141+
.multilineTextAlignment(.leading)
142+
}
143+
.padding(.vertical, 4)
144+
}
145+
}
121146
Section(header: Text("Security")) {
122147
VStack(alignment: .leading) {
123148
Text("Current OTP Code")
@@ -158,10 +183,30 @@ struct LoopAPNSBolusView: View {
158183
loadRecommendedBolus()
159184
// Reset timer state so it shows '-' until first tick
160185
otpTimeRemaining = nil
186+
// Don't reset TOTP usage flag here - let the timer handle it
187+
188+
// Validate TOTP state when view appears
189+
_ = isTOTPBlocked
161190
}
162191
.onReceive(otpTimer) { _ in
163192
let now = Date().timeIntervalSince1970
164-
otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
193+
let newOtpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
194+
195+
// Check if we've moved to a new TOTP period (when time remaining increases)
196+
if let currentOtpTimeRemaining = otpTimeRemaining,
197+
newOtpTimeRemaining > currentOtpTimeRemaining
198+
{
199+
// New TOTP code generated, reset the usage flag
200+
TOTPService.shared.resetTOTPUsage()
201+
}
202+
203+
// Also check if we're at the very beginning of a new period (when time remaining is close to 30)
204+
if newOtpTimeRemaining >= 29 {
205+
// We're at the start of a new TOTP period, reset the usage flag
206+
TOTPService.shared.resetTOTPUsage()
207+
}
208+
209+
otpTimeRemaining = newOtpTimeRemaining
165210

166211
// Check if recommended bolus calculation is older than 5 minutes (but less than 12 minutes)
167212
if let lastLoopTime = lastLoopTime {
@@ -332,6 +377,8 @@ struct LoopAPNSBolusView: View {
332377
DispatchQueue.main.async {
333378
isLoading = false
334379
if success {
380+
// Mark TOTP code as used
381+
TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
335382
alertMessage = "Insulin sent successfully!"
336383
alertType = .success
337384
LogManager.shared.log(

LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ struct LoopAPNSCarbsView: View {
2222
@FocusState private var carbsFieldIsFocused: Bool
2323
@FocusState private var absorptionFieldIsFocused: Bool
2424

25+
// Computed property to check if TOTP should be blocked
26+
private var isTOTPBlocked: Bool {
27+
TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
28+
}
29+
2530
enum AlertType {
2631
case success
2732
case error
@@ -168,10 +173,30 @@ struct LoopAPNSCarbsView: View {
168173
Text("Send Carbs")
169174
}
170175
}
171-
.disabled(carbsAmount.doubleValue(for: .gram()) <= 0 || isLoading)
176+
.disabled(carbsAmount.doubleValue(for: .gram()) <= 0 || isLoading || isTOTPBlocked)
172177
.frame(maxWidth: .infinity)
173178
}
174179

180+
// TOTP Blocking Warning Section
181+
if isTOTPBlocked {
182+
Section {
183+
VStack(alignment: .leading, spacing: 8) {
184+
HStack {
185+
Image(systemName: "exclamationmark.triangle.fill")
186+
.foregroundColor(.orange)
187+
Text("TOTP Code Already Used")
188+
.font(.headline)
189+
.foregroundColor(.orange)
190+
}
191+
Text("This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command.")
192+
.font(.caption)
193+
.foregroundColor(.secondary)
194+
.multilineTextAlignment(.leading)
195+
}
196+
.padding(.vertical, 4)
197+
}
198+
}
199+
175200
Section(header: Text("Security")) {
176201
VStack(alignment: .leading) {
177202
Text("Current OTP Code")
@@ -221,10 +246,30 @@ struct LoopAPNSCarbsView: View {
221246
}
222247
// Reset timer state so it shows '-' until first tick
223248
otpTimeRemaining = nil
249+
// Don't reset TOTP usage flag here - let the timer handle it
250+
251+
// Validate TOTP state when view appears
252+
_ = isTOTPBlocked
224253
}
225254
.onReceive(otpTimer) { _ in
226255
let now = Date().timeIntervalSince1970
227-
otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
256+
let newOtpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
257+
258+
// Check if we've moved to a new TOTP period (when time remaining increases)
259+
if let currentOtpTimeRemaining = otpTimeRemaining,
260+
newOtpTimeRemaining > currentOtpTimeRemaining
261+
{
262+
// New TOTP code generated, reset the usage flag
263+
TOTPService.shared.resetTOTPUsage()
264+
}
265+
266+
// Also check if we're at the very beginning of a new period (when time remaining is close to 30)
267+
if newOtpTimeRemaining >= 29 {
268+
// We're at the start of a new TOTP period, reset the usage flag
269+
TOTPService.shared.resetTOTPUsage()
270+
}
271+
272+
otpTimeRemaining = newOtpTimeRemaining
228273
}
229274
.alert(isPresented: $showAlert) {
230275
switch alertType {
@@ -345,6 +390,8 @@ struct LoopAPNSCarbsView: View {
345390
DispatchQueue.main.async {
346391
isLoading = false
347392
if success {
393+
// Mark TOTP code as used
394+
TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
348395
let timeFormatter = DateFormatter()
349396
timeFormatter.timeStyle = .short
350397
alertMessage = "Carbs sent successfully for \(timeFormatter.string(from: adjustedConsumedDate))!"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// LoopFollow
2+
// TOTPService.swift
3+
// Created by codebymini.
4+
5+
import Foundation
6+
7+
/// Service class for managing TOTP code usage and blocking logic
8+
class TOTPService {
9+
static let shared = TOTPService()
10+
11+
private init() {}
12+
13+
/// Checks if the current TOTP code is blocked (already used)
14+
/// - Parameter qrCodeURL: The QR code URL to extract the current TOTP from
15+
/// - Returns: True if the TOTP is blocked, false otherwise
16+
func isTOTPBlocked(qrCodeURL: String) -> Bool {
17+
guard let currentTOTP = TOTPGenerator.extractOTPFromURL(qrCodeURL) else {
18+
return false
19+
}
20+
21+
// Check if the current TOTP code equals the last sent TOTP code
22+
return currentTOTP == Observable.shared.lastSentTOTP.value
23+
}
24+
25+
/// Marks the current TOTP code as used
26+
/// - Parameter qrCodeURL: The QR code URL to extract the current TOTP from
27+
func markTOTPAsUsed(qrCodeURL: String) {
28+
if let currentTOTP = TOTPGenerator.extractOTPFromURL(qrCodeURL) {
29+
Observable.shared.lastSentTOTP.set(currentTOTP)
30+
}
31+
}
32+
33+
/// Resets the TOTP usage tracking (called when a new TOTP period starts)
34+
func resetTOTPUsage() {
35+
Observable.shared.lastSentTOTP.set(nil)
36+
}
37+
}

LoopFollow/Storage/Observable.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,9 @@ class Observable {
3535

3636
var settingsPath = ObservableValue<NavigationPath>(default: NavigationPath())
3737

38+
// MARK: - Loop APNS TOTP Tracking
39+
40+
var lastSentTOTP = ObservableValue<String?>(default: nil)
41+
3842
private init() {}
3943
}

0 commit comments

Comments
 (0)