Skip to content
Open
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
6 changes: 6 additions & 0 deletions tedee example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
D3B374722B8F5E1B00EDA7A5 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B3746C2B8F5E1B00EDA7A5 /* ContentViewModel.swift */; };
D3B374732B8F5E1B00EDA7A5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B3746D2B8F5E1B00EDA7A5 /* ContentView.swift */; };
D3B374942B8F700A00EDA7A5 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B374932B8F700A00EDA7A5 /* Configuration.swift */; };
D3B374952B8F800B00EDA7A5 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B374952B8F800A00EDA7A5 /* NotificationManager.swift */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -37,6 +38,7 @@
D3B3746C2B8F5E1B00EDA7A5 /* ContentViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = "<group>"; };
D3B3746D2B8F5E1B00EDA7A5 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
D3B374932B8F700A00EDA7A5 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
D3B374952B8F800A00EDA7A5 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -76,6 +78,7 @@
D3B3746C2B8F5E1B00EDA7A5 /* ContentViewModel.swift */,
D3B3746D2B8F5E1B00EDA7A5 /* ContentView.swift */,
D3B374932B8F700A00EDA7A5 /* Configuration.swift */,
D3B374952B8F800A00EDA7A5 /* NotificationManager.swift */,
);
path = "tedee example";
sourceTree = "<group>";
Expand Down Expand Up @@ -169,6 +172,7 @@
D3B3746F2B8F5E1B00EDA7A5 /* tedee_exampleApp.swift in Sources */,
D3B374942B8F700A00EDA7A5 /* Configuration.swift in Sources */,
D3B374732B8F5E1B00EDA7A5 /* ContentView.swift in Sources */,
D3B374952B8F800B00EDA7A5 /* NotificationManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -308,6 +312,7 @@
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is required to make connection to tedee Lock";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIBackgroundModes = "bluetooth-central";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
Expand Down Expand Up @@ -338,6 +343,7 @@
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is required to make connection to tedee Lock";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIBackgroundModes = "bluetooth-central";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
Expand Down
35 changes: 31 additions & 4 deletions tedee example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,18 @@ struct ContentView: View {
Text("Pull")
}
.buttonStyle(.bordered)

Spacer()

Button {
viewModel.openLock()
} label: {
Text("Open")
}
.buttonStyle(.bordered)

Spacer()

Button {
viewModel.closeLock()
} label: {
Expand All @@ -119,6 +119,33 @@ struct ContentView: View {
Spacer()
}
}

Section {
VStack {
Text("Persistent Notification")
.font(.headline)
HStack {
Button {
Task {
try? await NotificationManager.shared.showPersistentNotification()
}
} label: {
Text("Show Notification")
}
.buttonStyle(.bordered)

Spacer()

Button {
NotificationManager.shared.removePersistentNotification()
} label: {
Text("Hide Notification")
.foregroundStyle(.red)
}
.buttonStyle(.bordered)
}
}
}

Section {
VStack(alignment: .leading) {
Expand Down
37 changes: 36 additions & 1 deletion tedee example/ContentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ final class ContentViewModel {
}
}
}

Task { @MainActor in
for await notification in TedeeLockManager.shared.notificationsStream {
if self.serialNumber.serialNumber == notification.serialNumber.serialNumber {
Expand All @@ -169,4 +169,39 @@ final class ContentViewModel {
}
}
}

@MainActor
func handleNotificationAction(_ actionIdentifier: String) async {
comunicationList.append(ComunicationListItem("notification action: \(actionIdentifier)"))

// Auto-connect if not connected
if connectionStatus != .connected {
comunicationList.append(ComunicationListItem("auto-connecting to lock..."))
await connect()

// Wait for connection to establish
try? await Task.sleep(for: .seconds(2))

// Check if connected
if connectionStatus != .connected {
comunicationList.append(ComunicationListItem("failed to auto-connect"))
return
}
}

// Execute action based on identifier
switch actionIdentifier {
case NotificationManager.openActionIdentifier:
comunicationList.append(ComunicationListItem("executing open from notification"))
await openLock()
case NotificationManager.closeActionIdentifier:
comunicationList.append(ComunicationListItem("executing close from notification"))
await closeLock()
case NotificationManager.pullSpringActionIdentifier:
comunicationList.append(ComunicationListItem("executing pull spring from notification"))
await pullLock()
default:
break
}
}
}
123 changes: 123 additions & 0 deletions tedee example/NotificationManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// NotificationManager.swift
// tedee example
//
// Created for persistent lock control notification
//

import Foundation
import UserNotifications
import UIKit

@MainActor
class NotificationManager: NSObject, ObservableObject, UNUserNotificationCenterDelegate {
static let shared = NotificationManager()

// Action identifiers
static let openActionIdentifier = "OPEN_LOCK"
static let closeActionIdentifier = "CLOSE_LOCK"
static let pullSpringActionIdentifier = "PULL_SPRING"

// Notification identifier
private let persistentNotificationIdentifier = "tedee_lock_control"

// Callback for handling actions
var onActionReceived: ((String) async -> Void)?

private override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
}

// Request notification permissions
func requestAuthorization() async throws {
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
if !granted {
throw NSError(domain: "NotificationManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Notification permission denied"])
}
}

// Setup notification categories with actions
func setupNotificationCategories() {
let openAction = UNNotificationAction(
identifier: Self.openActionIdentifier,
title: "🔓 Apri",
options: [.foreground]
)

let closeAction = UNNotificationAction(
identifier: Self.closeActionIdentifier,
title: "🔒 Chiudi",
options: [.foreground]
)

let pullSpringAction = UNNotificationAction(
identifier: Self.pullSpringActionIdentifier,
title: "🔄 Pull Spring",
options: [.foreground]
)

let category = UNNotificationCategory(
identifier: "LOCK_CONTROL",
actions: [openAction, closeAction, pullSpringAction],
intentIdentifiers: [],
options: []
)

UNUserNotificationCenter.current().setNotificationCategories([category])
}

// Show persistent notification
func showPersistentNotification() async throws {
let content = UNMutableNotificationContent()
content.title = "Tedee Lock Control"
content.body = "Controllo rapido del tuo lock"
content.categoryIdentifier = "LOCK_CONTROL"
content.sound = nil

// Create a trigger that fires immediately
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)

let request = UNNotificationRequest(
identifier: persistentNotificationIdentifier,
content: content,
trigger: trigger
)

try await UNUserNotificationCenter.current().add(request)
}

// Remove persistent notification
func removePersistentNotification() {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [persistentNotificationIdentifier])
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [persistentNotificationIdentifier])
}

// MARK: - UNUserNotificationCenterDelegate

// Handle notification when app is in foreground
nonisolated func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
return [.banner, .list]
}

// Handle notification action response
nonisolated func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
let actionIdentifier = response.actionIdentifier

// Call the action handler on main actor
await MainActor.run {
Task {
await self.onActionReceived?(actionIdentifier)

// Re-show the notification after action
try? await self.showPersistentNotification()
}
}
}
}
27 changes: 26 additions & 1 deletion tedee example/tedee_exampleApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,34 @@ import SwiftUI

@main
struct tedee_exampleApp: App {
@StateObject private var viewModel = ContentViewModel()

var body: some Scene {
WindowGroup {
ContentView(viewModel: ContentViewModel())
ContentView(viewModel: viewModel)
.task {
// Initialize notification manager on main thread
let notificationManager = NotificationManager.shared

// Setup notification categories
notificationManager.setupNotificationCategories()

// Request authorization
do {
try await notificationManager.requestAuthorization()

// Show persistent notification
try await notificationManager.showPersistentNotification()
} catch {
print("Notification setup error: \(error)")
}

// Setup notification action handler
NotificationManager.shared.onActionReceived = { actionIdentifier in
await self.viewModel.handleNotificationAction(actionIdentifier)
}
}
}
}
}