Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions LoopFollow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
654132E72E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */; };
654132EA2E19F24800BDBE08 /* TOTPGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E92E19F24800BDBE08 /* TOTPGenerator.swift */; };
6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */; };
6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134192E1DC27900BDBE08 /* OverridePresetData.swift */; };
6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; };
DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; };
DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; };
DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; };
Expand Down Expand Up @@ -393,9 +394,10 @@
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>"; };
654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleQRCodeScannerView.swift; sourceTree = "<group>"; };
654132E92E19F24800BDBE08 /* TOTPGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPGenerator.swift; sourceTree = "<group>"; };
6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
654134172E1DC09700BDBE08 /* OverridePresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetsView.swift; sourceTree = "<group>"; };
654134192E1DC27900BDBE08 /* OverridePresetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetData.swift; sourceTree = "<group>"; };
6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = "<group>"; };
A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = "<group>"; };
DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1191,6 +1193,7 @@
DDEF503E2D479B8A00884336 /* LoopAPNS */ = {
isa = PBXGroup;
children = (
6584B1002E4A263900135D4D /* TOTPService.swift */,
DDEF503F2D479B8A00884336 /* LoopAPNSService.swift */,
DDEF50412D479BAA00884336 /* LoopAPNSCarbsView.swift */,
DDEF50422D479BBA00884336 /* LoopAPNSBolusView.swift */,
Expand Down Expand Up @@ -1936,6 +1939,7 @@
DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */,
DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */,
DD0650F32DCE9B3D004D3B41 /* MissedReadingEditor.swift in Sources */,
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */,
DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */,
DDEF503A2D31615000999A5D /* LogManager.swift in Sources */,
DD4878172C7B75350048F05C /* BolusView.swift in Sources */,
Expand Down
51 changes: 49 additions & 2 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ struct LoopAPNSBolusView: View {
private let otpPeriod: TimeInterval = 30
private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

// Computed property to check if TOTP should be blocked
private var isTOTPBlocked: Bool {
TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
}

enum AlertType {
case success
case error
Expand Down Expand Up @@ -116,9 +121,29 @@ struct LoopAPNSBolusView: View {
Text("Send Insulin")
}
}
.disabled(insulinAmount.doubleValue(for: .internationalUnit()) <= 0 || isLoading)
.disabled(insulinAmount.doubleValue(for: .internationalUnit()) <= 0 || isLoading || isTOTPBlocked)
.frame(maxWidth: .infinity)
}

// TOTP Blocking Warning Section
if isTOTPBlocked {
Section {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text("TOTP Code Already Used")
.font(.headline)
.foregroundColor(.orange)
}
Text("This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
}
.padding(.vertical, 4)
}
}
Section(header: Text("Security")) {
VStack(alignment: .leading) {
Text("Current OTP Code")
Expand Down Expand Up @@ -159,10 +184,30 @@ struct LoopAPNSBolusView: View {
loadRecommendedBolus()
// Reset timer state so it shows '-' until first tick
otpTimeRemaining = nil
// Don't reset TOTP usage flag here - let the timer handle it

// Validate TOTP state when view appears
_ = isTOTPBlocked
}
.onReceive(otpTimer) { _ in
let now = Date().timeIntervalSince1970
otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
let newOtpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))

// Check if we've moved to a new TOTP period (when time remaining increases)
if let currentOtpTimeRemaining = otpTimeRemaining,
newOtpTimeRemaining > currentOtpTimeRemaining
{
// New TOTP code generated, reset the usage flag
TOTPService.shared.resetTOTPUsage()
}

// Also check if we're at the very beginning of a new period (when time remaining is close to 30)
if newOtpTimeRemaining >= 29 {
// We're at the start of a new TOTP period, reset the usage flag
TOTPService.shared.resetTOTPUsage()
}

otpTimeRemaining = newOtpTimeRemaining

// Check if recommended bolus calculation is older than 5 minutes (but less than 12 minutes)
if let lastLoopTime = lastLoopTime {
Expand Down Expand Up @@ -333,6 +378,8 @@ struct LoopAPNSBolusView: View {
DispatchQueue.main.async {
isLoading = false
if success {
// Mark TOTP code as used
TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
alertMessage = "Insulin sent successfully!"
alertType = .success
LogManager.shared.log(
Expand Down
51 changes: 49 additions & 2 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ struct LoopAPNSCarbsView: View {
@FocusState private var carbsFieldIsFocused: Bool
@FocusState private var absorptionFieldIsFocused: Bool

// Computed property to check if TOTP should be blocked
private var isTOTPBlocked: Bool {
TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
}

enum AlertType {
case success
case error
Expand Down Expand Up @@ -169,10 +174,30 @@ struct LoopAPNSCarbsView: View {
Text("Send Carbs")
}
}
.disabled(carbsAmount.doubleValue(for: .gram()) <= 0 || isLoading)
.disabled(carbsAmount.doubleValue(for: .gram()) <= 0 || isLoading || isTOTPBlocked)
.frame(maxWidth: .infinity)
}

// TOTP Blocking Warning Section
if isTOTPBlocked {
Section {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text("TOTP Code Already Used")
.font(.headline)
.foregroundColor(.orange)
}
Text("This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
}
.padding(.vertical, 4)
}
}

Section(header: Text("Security")) {
VStack(alignment: .leading) {
Text("Current OTP Code")
Expand Down Expand Up @@ -222,10 +247,30 @@ struct LoopAPNSCarbsView: View {
}
// Reset timer state so it shows '-' until first tick
otpTimeRemaining = nil
// Don't reset TOTP usage flag here - let the timer handle it

// Validate TOTP state when view appears
_ = isTOTPBlocked
}
.onReceive(otpTimer) { _ in
let now = Date().timeIntervalSince1970
otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
let newOtpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))

// Check if we've moved to a new TOTP period (when time remaining increases)
if let currentOtpTimeRemaining = otpTimeRemaining,
newOtpTimeRemaining > currentOtpTimeRemaining
{
// New TOTP code generated, reset the usage flag
TOTPService.shared.resetTOTPUsage()
}

// Also check if we're at the very beginning of a new period (when time remaining is close to 30)
if newOtpTimeRemaining >= 29 {
// We're at the start of a new TOTP period, reset the usage flag
TOTPService.shared.resetTOTPUsage()
}

otpTimeRemaining = newOtpTimeRemaining
}
.alert(isPresented: $showAlert) {
switch alertType {
Expand Down Expand Up @@ -346,6 +391,8 @@ struct LoopAPNSCarbsView: View {
DispatchQueue.main.async {
isLoading = false
if success {
// Mark TOTP code as used
TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
let timeFormatter = DateFormatter()
timeFormatter.timeStyle = .short
alertMessage = "Carbs sent successfully for \(timeFormatter.string(from: adjustedConsumedDate))!"
Expand Down
37 changes: 37 additions & 0 deletions LoopFollow/Remote/LoopAPNS/TOTPService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// LoopFollow
// TOTPService.swift
// Created by codebymini.

import Foundation

/// Service class for managing TOTP code usage and blocking logic
class TOTPService {
static let shared = TOTPService()

private init() {}

/// Checks if the current TOTP code is blocked (already used)
/// - Parameter qrCodeURL: The QR code URL to extract the current TOTP from
/// - Returns: True if the TOTP is blocked, false otherwise
func isTOTPBlocked(qrCodeURL: String) -> Bool {
guard let currentTOTP = TOTPGenerator.extractOTPFromURL(qrCodeURL) else {
return false
}

// Check if the current TOTP code equals the last sent TOTP code
return currentTOTP == Observable.shared.lastSentTOTP.value
}

/// Marks the current TOTP code as used
/// - Parameter qrCodeURL: The QR code URL to extract the current TOTP from
func markTOTPAsUsed(qrCodeURL: String) {
if let currentTOTP = TOTPGenerator.extractOTPFromURL(qrCodeURL) {
Observable.shared.lastSentTOTP.set(currentTOTP)
}
}

/// Resets the TOTP usage tracking (called when a new TOTP period starts)
func resetTOTPUsage() {
Observable.shared.lastSentTOTP.set(nil)
}
}
4 changes: 4 additions & 0 deletions LoopFollow/Storage/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ class Observable {

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

// MARK: - Loop APNS TOTP Tracking

var lastSentTOTP = ObservableValue<String?>(default: nil)

private init() {}
}