Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
136 changes: 82 additions & 54 deletions LoopFollow.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions LoopFollow/Helpers/AppConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,34 @@ import Foundation
// Class that contains general constants used in different classes
class AppConstants {
static let APP_GROUP_ID = "group.com.$(unique_id).LoopFollow"

/// Extracts the app suffix from the bundle identifier
/// Bundle identifier format: com.$(unique_id).LoopFollow$(app_suffix)
/// Returns the suffix part (e.g., "2" for "com.example.LoopFollow2")
static var appSuffix: String {
guard let bundleId = Bundle.main.bundleIdentifier else {
return ""
}

// Extract suffix from bundle identifier
// Pattern: com.$(unique_id).LoopFollow$(app_suffix)
let pattern = "LoopFollow(.+)$"
if let regex = try? NSRegularExpression(pattern: pattern) {
let range = NSRange(location: 0, length: bundleId.utf16.count)
if let match = regex.firstMatch(in: bundleId, options: [], range: range) {
let suffixRange = match.range(at: 1)
if let swiftRange = Range(suffixRange, in: bundleId) {
let suffix = String(bundleId[swiftRange])
return suffix.isEmpty ? "" : "_\(suffix)"
}
}
}

return ""
}

/// Returns a unique identifier for this app instance based on the app suffix
static var appInstanceId: String {
return "LoopFollow\(appSuffix)"
}
}
6 changes: 0 additions & 6 deletions LoopFollow/Helpers/Views/QRCodeDisplayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ struct QRCodeDisplayView: View {
.scaleEffect(1.5)
)
}

Text("Scan this QR code with another LoopFollow app to import remote command settings")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
}
.onAppear {
generateQRCode()
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Remote/Settings/RemoteCommandSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ struct RemoteCommandSettings: Codable {
}

/// Checks if the settings are valid for the given remote type
func isValid() -> Bool {
func hasValidSettings() -> Bool {
switch remoteType {
case .none:
return true
Expand Down
83 changes: 11 additions & 72 deletions LoopFollow/Remote/Settings/RemoteSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ struct RemoteSettingsView: View {

enum AlertType {
case validation
case qrCodeError
case urlTokenValidation
case urlTokenUpdate
}
Expand Down Expand Up @@ -61,34 +60,21 @@ struct RemoteSettingsView: View {
.foregroundColor(.secondary)
}

// MARK: - QR Code Sharing Section
// MARK: - Import/Export Settings Section

Section {
if viewModel.remoteType == .none {
Button(action: {
viewModel.isShowingQRCodeScanner = true
}) {
HStack {
Image(systemName: "qrcode.viewfinder")
Text("Import Remote Settings from QR Code")
}
}
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
} else {
Button(action: {
viewModel.isShowingQRCodeDisplay = true
}) {
HStack {
Image(systemName: "qrcode")
Text("Export Remote Settings as QR Code")
}
NavigationLink(destination: ImportExportSettingsView()) {
HStack {
Image(systemName: "square.and.arrow.down")
.foregroundColor(.blue)
Text("Import/Export Settings")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
}
.buttonStyle(.plain)
}

// MARK: - Meal Section (for TRC only)
Expand Down Expand Up @@ -294,12 +280,6 @@ struct RemoteSettingsView: View {
message: Text(alertMessage ?? "Invalid input."),
dismissButton: .default(Text("OK"))
)
case .qrCodeError:
return Alert(
title: Text("QR Code Error"),
message: Text(alertMessage ?? "An error occurred while processing the QR code."),
dismissButton: .default(Text("OK"))
)
case .urlTokenValidation:
return Alert(
title: Text("URL/Token Validation"),
Expand All @@ -325,30 +305,6 @@ struct RemoteSettingsView: View {
viewModel.handleLoopAPNSQRCodeScanResult(result)
}
}
.sheet(isPresented: $viewModel.isShowingQRCodeScanner) {
SimpleQRCodeScannerView { result in
viewModel.handleRemoteCommandQRCodeScanResult(result)
}
}
.sheet(isPresented: $viewModel.isShowingQRCodeDisplay) {
NavigationView {
VStack {
if let qrCodeString = viewModel.generateQRCodeForCurrentSettings() {
QRCodeDisplayView(qrCodeString: qrCodeString)
.padding()
} else {
Text("Failed to generate QR code")
.foregroundColor(.red)
.padding()
}
}
.navigationTitle("Share Remote Settings")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: Button("Done") {
viewModel.isShowingQRCodeDisplay = false
})
}
}
.sheet(isPresented: $viewModel.showURLTokenValidation) {
NavigationView {
URLTokenValidationView(
Expand Down Expand Up @@ -380,15 +336,6 @@ struct RemoteSettingsView: View {
let now = Date().timeIntervalSince1970
otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
}
.onReceive(viewModel.$qrCodeErrorMessage) { errorMessage in
if let errorMessage = errorMessage, !errorMessage.isEmpty {
handleQRCodeError(errorMessage)
// Clear the error message after showing the alert
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
viewModel.qrCodeErrorMessage = nil
}
}
}
.onReceive(viewModel.$showURLTokenValidation) { showValidation in
if showValidation {
// The sheet will be shown automatically due to the binding
Expand Down Expand Up @@ -429,14 +376,6 @@ struct RemoteSettingsView: View {
showAlert = true
}

// MARK: - QR Code Error Handler

private func handleQRCodeError(_ message: String) {
alertMessage = message
alertType = .qrCodeError
showAlert = true
}

private var guardrailsSection: some View {
Section(header: Text("Guardrails")) {
HStack {
Expand Down
48 changes: 0 additions & 48 deletions LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ class RemoteSettingsViewModel: ObservableObject {
@Published var isShowingLoopAPNSScanner: Bool = false
@Published var loopAPNSErrorMessage: String?

// MARK: - QR Code Sharing Properties

@Published var isShowingQRCodeScanner: Bool = false
@Published var isShowingQRCodeDisplay: Bool = false
@Published var qrCodeErrorMessage: String?

// MARK: - URL/Token Validation Properties

@Published var pendingSettings: RemoteCommandSettings?
Expand Down Expand Up @@ -234,48 +228,6 @@ class RemoteSettingsViewModel: ObservableObject {
}
}

// MARK: - QR Code Sharing Methods

func handleRemoteCommandQRCodeScanResult(_ result: Result<String, Error>) {
DispatchQueue.main.async {
switch result {
case let .success(jsonString):
if let settings = RemoteCommandSettings.decodeFromJSON(jsonString) {
if settings.isValid() {
// Check URL and token compatibility
let validation = settings.validateCompatibilityWithCurrentStorage()

if validation.isCompatible {
// No conflicts, apply settings directly
settings.applyToStorage()
self.updateViewModelFromStorage()
LogManager.shared.log(category: .remote, message: "Remote command settings imported from QR code")
} else {
// Conflicts detected, show validation view
self.pendingSettings = settings
self.validationMessage = validation.message
self.shouldPromptForURL = validation.shouldPromptForURL
self.shouldPromptForToken = validation.shouldPromptForToken
self.showURLTokenValidation = true
}
} else {
self.qrCodeErrorMessage = "Invalid remote command settings in QR code"
}
} else {
self.qrCodeErrorMessage = "Failed to decode remote command settings from QR code"
}
case let .failure(error):
self.qrCodeErrorMessage = "Scanning failed: \(error.localizedDescription)"
}
self.isShowingQRCodeScanner = false
}
}

func generateQRCodeForCurrentSettings() -> String? {
let settings = RemoteCommandSettings.fromCurrentStorage()
return settings.encodeToJSON()
}

// MARK: - Public Methods for View Access

/// Updates the view model properties from storage (accessible from view)
Expand Down
Loading