From 8f103fab3e77325e97db6989e6de67b06d960f33 Mon Sep 17 00:00:00 2001 From: Andy Pack Date: Fri, 9 Dec 2022 08:51:42 +0000 Subject: [PATCH] initial notifications support --- Mixonomer.xcodeproj/project.pbxproj | 26 +++- .../xcschemes/Mixonomer - Local.xcscheme | 2 +- Mixonomer/Application/AppDelegate.swift | 20 +++ .../AppIcon.appiconset/Contents.json | 134 ++++++++++-------- Mixonomer/Info.plist | 4 + Mixonomer/Mixonomer.entitlements | 4 + Mixonomer/Model/LiveUser.swift | 65 +++++---- Mixonomer/Model/Playlist.swift | 40 +++--- Mixonomer/Model/User.swift | 87 ++++++++++-- Mixonomer/Network/NetworkHelper.swift | 38 +++++ Mixonomer/Network/UserApi.swift | 41 ++++-- Mixonomer/Notifications/APNSHandler.swift | 37 +++++ Mixonomer/Notifications/StaticNotif.swift | 14 ++ Mixonomer/Views/Admin/UsersList.swift | 2 +- Mixonomer/Views/AppSkeleton.swift | 2 +- Mixonomer/Views/LoginScreen.swift | 6 +- .../Views/Playlist/AddPlaylistSheet.swift | 2 +- Mixonomer/Views/Playlist/PlaylistList.swift | 6 +- Mixonomer/Views/Playlist/PlaylistRow.swift | 2 +- Mixonomer/Views/Playlist/PlaylistView.swift | 4 +- .../Settings/NotificationsControls.swift | 52 +++++++ Mixonomer/Views/Settings/SettingsList.swift | 32 ++--- Mixonomer/Views/Tag/AddTagSheet.swift | 2 +- Mixonomer/Views/Tag/TagList.swift | 2 +- Mixonomer/Views/Tag/TagRow.swift | 2 +- Mixonomer/Views/Tag/TagView.swift | 19 ++- 26 files changed, 480 insertions(+), 165 deletions(-) create mode 100644 Mixonomer/Network/NetworkHelper.swift create mode 100644 Mixonomer/Notifications/APNSHandler.swift create mode 100644 Mixonomer/Notifications/StaticNotif.swift create mode 100644 Mixonomer/Views/Settings/NotificationsControls.swift diff --git a/Mixonomer.xcodeproj/project.pbxproj b/Mixonomer.xcodeproj/project.pbxproj index b57a566..cef3b41 100644 --- a/Mixonomer.xcodeproj/project.pbxproj +++ b/Mixonomer.xcodeproj/project.pbxproj @@ -10,6 +10,10 @@ A10C8D29281302050018AE12 /* ToastUI in Frameworks */ = {isa = PBXBuildFile; productRef = A10C8D28281302050018AE12 /* ToastUI */; }; A11AC70628A188AE00645043 /* AuthApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11AC70528A188AE00645043 /* AuthApi.swift */; }; A13C54972928FD7C0034F233 /* ManagedInputList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13C54962928FD7C0034F233 /* ManagedInputList.swift */; }; + A15D257A293421350049055E /* StaticNotif.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D2579293421350049055E /* StaticNotif.swift */; }; + A15D257C293425390049055E /* NotificationsControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D257B293425390049055E /* NotificationsControls.swift */; }; + A15D257E29342E4F0049055E /* APNSHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D257D29342E4F0049055E /* APNSHandler.swift */; }; + A15D258029342EF50049055E /* NetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D257F29342EF50049055E /* NetworkHelper.swift */; }; A1AF726F28A84F7D00D317C9 /* AdminApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF726E28A84F7D00D317C9 /* AdminApi.swift */; }; A1AF727128A850AE00D317C9 /* UsersList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF727028A850AE00D317C9 /* UsersList.swift */; }; A1AF727328A9062600D317C9 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF727228A9062600D317C9 /* UserView.swift */; }; @@ -71,6 +75,10 @@ A11AC70528A188AE00645043 /* AuthApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthApi.swift; sourceTree = ""; }; A13C54962928FD7C0034F233 /* ManagedInputList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedInputList.swift; sourceTree = ""; }; A146915A28118F940052999D /* Mixonomer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mixonomer.entitlements; sourceTree = ""; }; + A15D2579293421350049055E /* StaticNotif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticNotif.swift; sourceTree = ""; }; + A15D257B293425390049055E /* NotificationsControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsControls.swift; sourceTree = ""; }; + A15D257D29342E4F0049055E /* APNSHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSHandler.swift; sourceTree = ""; }; + A15D257F29342EF50049055E /* NetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHelper.swift; sourceTree = ""; }; A1AF726E28A84F7D00D317C9 /* AdminApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminApi.swift; sourceTree = ""; }; A1AF727028A850AE00D317C9 /* UsersList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersList.swift; sourceTree = ""; }; A1AF727228A9062600D317C9 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = ""; }; @@ -143,6 +151,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + A15D2578293421250049055E /* Notifications */ = { + isa = PBXGroup; + children = ( + A15D2579293421350049055E /* StaticNotif.swift */, + A15D257D29342E4F0049055E /* APNSHandler.swift */, + ); + path = Notifications; + sourceTree = ""; + }; A1DBCDA428A5184D002CF730 /* Admin */ = { isa = PBXGroup; children = ( @@ -171,9 +188,10 @@ E97AF45F23FC85D600635494 /* PlaylistApi.swift */, E97AF45A23FC748D00635494 /* UserApi.swift */, E9E30C2523FEA4EF00574EEF /* TagApi.swift */, - E906F7F32414019C004E1E31 /* NetworkPersister.swift */, A11AC70528A188AE00645043 /* AuthApi.swift */, A1AF726E28A84F7D00D317C9 /* AdminApi.swift */, + E906F7F32414019C004E1E31 /* NetworkPersister.swift */, + A15D257F29342EF50049055E /* NetworkHelper.swift */, ); path = Network; sourceTree = ""; @@ -230,6 +248,7 @@ isa = PBXGroup; children = ( E9E30C3223FF255C00574EEF /* SettingsList.swift */, + A15D257B293425390049055E /* NotificationsControls.swift */, ); path = Settings; sourceTree = ""; @@ -257,6 +276,7 @@ E9EA690923F9A5430012C3E8 /* Mixonomer */ = { isa = PBXGroup; children = ( + A15D2578293421250049055E /* Notifications */, A146915A28118F940052999D /* Mixonomer.entitlements */, E98254C623FA25280056D9D3 /* Application */, E9EA691023F9A54A0012C3E8 /* Assets.xcassets */, @@ -448,6 +468,7 @@ E98254CA23FA26600056D9D3 /* PlaylistRow.swift in Sources */, E9EA690B23F9A5430012C3E8 /* AppDelegate.swift in Sources */, E906F7F42414019C004E1E31 /* NetworkPersister.swift in Sources */, + A15D258029342EF50049055E /* NetworkHelper.swift in Sources */, A1AF727328A9062600D317C9 /* UserView.swift in Sources */, E9E30C3323FF255C00574EEF /* SettingsList.swift in Sources */, E9EA690D23F9A5430012C3E8 /* SceneDelegate.swift in Sources */, @@ -459,8 +480,10 @@ E97AF46023FC85D600635494 /* PlaylistApi.swift in Sources */, A11AC70628A188AE00645043 /* AuthApi.swift in Sources */, A1AF726F28A84F7D00D317C9 /* AdminApi.swift in Sources */, + A15D257E29342E4F0049055E /* APNSHandler.swift in Sources */, A1AF727128A850AE00D317C9 /* UsersList.swift in Sources */, E9EA690F23F9A5430012C3E8 /* AppSkeleton.swift in Sources */, + A15D257A293421350049055E /* StaticNotif.swift in Sources */, E98254BD23F9B7A90056D9D3 /* Playlist.swift in Sources */, A13C54972928FD7C0034F233 /* ManagedInputList.swift in Sources */, E97AF46723FD650800635494 /* AddPlaylistSheet.swift in Sources */, @@ -471,6 +494,7 @@ E98254D023FB00B60056D9D3 /* LoginScreen.swift in Sources */, E9E30C2623FEA4F000574EEF /* TagApi.swift in Sources */, E97AF46923FD9E1B00635494 /* SpotInputList.swift in Sources */, + A15D257C293425390049055E /* NotificationsControls.swift in Sources */, E97AF45B23FC748D00635494 /* UserApi.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mixonomer.xcodeproj/xcshareddata/xcschemes/Mixonomer - Local.xcscheme b/Mixonomer.xcodeproj/xcshareddata/xcschemes/Mixonomer - Local.xcscheme index 80c0f30..1eb4451 100644 --- a/Mixonomer.xcodeproj/xcshareddata/xcschemes/Mixonomer - Local.xcscheme +++ b/Mixonomer.xcodeproj/xcshareddata/xcschemes/Mixonomer - Local.xcscheme @@ -53,7 +53,7 @@ diff --git a/Mixonomer/Application/AppDelegate.swift b/Mixonomer/Application/AppDelegate.swift index d42f9de..d86865d 100644 --- a/Mixonomer/Application/AppDelegate.swift +++ b/Mixonomer/Application/AppDelegate.swift @@ -15,6 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. + UIApplication.shared.registerForRemoteNotifications() return true } @@ -31,8 +32,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } + + func application(_ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken + deviceToken: Data) { + + let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() + + StaticNotif.token = token + } + func application(_ application: UIApplication, + didFailToRegisterForRemoteNotificationsWithError + error: Error) { + // Try again later. + } + func application(_ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable : Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + completionHandler(.noData) + } } extension Logger { diff --git a/Mixonomer/Assets.xcassets/AppIcon.appiconset/Contents.json b/Mixonomer/Assets.xcassets/AppIcon.appiconset/Contents.json index 04de9d4..f78687a 100644 --- a/Mixonomer/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Mixonomer/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -150,6 +150,66 @@ "scale" : "1x", "size" : "1024x1024" }, + { + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + }, { "filename" : "48.png", "idiom" : "watch", @@ -225,6 +285,13 @@ "size" : "51x51", "subtype" : "45mm" }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, { "filename" : "172.png", "idiom" : "watch", @@ -256,71 +323,18 @@ "size" : "117x117", "subtype" : "45mm" }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, { "filename" : "1024.png", "idiom" : "watch-marketing", "scale" : "1x", "size" : "1024x1024" - }, - { - "filename" : "16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "64.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "128.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "1024.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" } ], "info" : { diff --git a/Mixonomer/Info.plist b/Mixonomer/Info.plist index 4dd0877..d6fc4a1 100644 --- a/Mixonomer/Info.plist +++ b/Mixonomer/Info.plist @@ -43,6 +43,10 @@ UILaunchStoryboardName LaunchScreen + UIBackgroundModes + + remote-notification + UIRequiredDeviceCapabilities armv7 diff --git a/Mixonomer/Mixonomer.entitlements b/Mixonomer/Mixonomer.entitlements index 225aa48..b0eb413 100644 --- a/Mixonomer/Mixonomer.entitlements +++ b/Mixonomer/Mixonomer.entitlements @@ -2,6 +2,10 @@ + aps-environment + development + com.apple.developer.aps-environment + development com.apple.security.app-sandbox com.apple.security.network.client diff --git a/Mixonomer/Model/LiveUser.swift b/Mixonomer/Model/LiveUser.swift index ce75b49..5b8bb29 100644 --- a/Mixonomer/Model/LiveUser.swift +++ b/Mixonomer/Model/LiveUser.swift @@ -11,13 +11,14 @@ import Alamofire import SwiftyJSON import KeychainAccess import OSLog +import UserNotifications class LiveUser: ObservableObject { @Published var playlists: [Playlist] @Published var tags: [Tag] @Published var username: String - @Published var user: User? + @Published var user: User @Published var loggedIn: Bool { didSet { @@ -25,6 +26,27 @@ class LiveUser: ObservableObject { } } + func requestAPNSPerms(){ + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + + if error != nil { + self.handleAPNSFailure() + } + else { + + // load token from static var and pass to backend server + APNSHandler.pass_token_to_backend(onFailure: { + self.handleAPNSFailure() + }) + } + } + } + + func handleAPNSFailure(){ + Logger.sys.debug("failed to get APNS token") + } + @Published var isRefreshingUser = false @Published var isRefreshingPlaylists = false @Published var isRefreshingTags = false @@ -34,6 +56,7 @@ class LiveUser: ObservableObject { self.tags = tags self.username = username self.loggedIn = loggedIn + self.user = User.get_null_user() } init(playlists: [Playlist], tags: [Tag], username: String, loggedIn: Bool, user: User) { @@ -45,11 +68,7 @@ class LiveUser: ObservableObject { } func lastfm_connected() -> Bool { - if let username = user?.lastfm_username { - return username.count > 0 - } - - return false + return username.count > 0 } func logout() { @@ -64,7 +83,7 @@ class LiveUser: ObservableObject { playlists.removeAll() tags.removeAll() username = "" - user = nil + user = User.get_null_user() UserDefaults.standard.removeObject(forKey: "playlists") UserDefaults.standard.removeObject(forKey: "tags") @@ -224,27 +243,9 @@ class LiveUser: ObservableObject { } func check_network_response(response: AFDataResponse) -> Bool { - - if let statusCode = response.response?.statusCode { - switch statusCode { - case 401: // token has expired - Logger.sys.info("token expired, logging user out") - self.logout() - return false - case 400..<500: - Logger.net.error("client fault \(statusCode)") - return false - case 500..<600: - Logger.net.warning("server fault \(statusCode)") - return false - case _: // 200 -> Success - return true - } - } - - Logger.net.error("live user failed to access status code to check") - - return false + return NetworkHelper.check_network_response(response: response, onTokenFail: { + self.logout() + }) } func load_user_defaults() -> LiveUser { @@ -277,4 +278,12 @@ class LiveUser: ObservableObject { return self } + + static func get_preview_user() -> LiveUser { + return LiveUser(playlists: [], tags: [], username: "user", loggedIn: false) + } + + static func get_preview_user_with_user() -> LiveUser { + return LiveUser(playlists: [], tags: [], username: "user", loggedIn: false, user: User()) + } } diff --git a/Mixonomer/Model/Playlist.swift b/Mixonomer/Model/Playlist.swift index 6120587..7976118 100644 --- a/Mixonomer/Model/Playlist.swift +++ b/Mixonomer/Model/Playlist.swift @@ -30,28 +30,28 @@ class Playlist: Identifiable, Equatable, Codable, ObservableObject { self.updatePlaylist(updates: JSON(["include_recommendations": self.include_recommendations])) } } - @Published var recommendation_sample: Int{ + @Published var recommendation_sample: Int { didSet { self.updatePlaylist(updates: JSON(["recommendation_sample": self.recommendation_sample])) } } - @Published var include_library_tracks: Bool{ + @Published var include_library_tracks: Bool { didSet { self.updatePlaylist(updates: JSON(["include_library_tracks": self.include_library_tracks])) } } - @Published var parts: Array{ + @Published var parts: Array { didSet { self.updatePlaylist(updates: JSON(["parts": self.parts])) } } - @Published var playlist_references: Array{ + @Published var playlist_references: Array { didSet { self.updatePlaylist(updates: JSON(["playlist_references": self.playlist_references])) } } - @Published var shuffle: Bool{ + @Published var shuffle: Bool { didSet { self.updatePlaylist(updates: JSON(["shuffle": self.shuffle])) } @@ -88,45 +88,45 @@ class Playlist: Identifiable, Equatable, Codable, ObservableObject { @Published var lastfm_stat_last_refresh: String? - @Published var add_last_month: Bool{ + @Published var add_last_month: Bool { didSet { self.updatePlaylist(updates: JSON(["add_last_month": self.add_last_month])) } } - @Published var add_this_month: Bool{ + @Published var add_this_month: Bool { didSet { self.updatePlaylist(updates: JSON(["add_this_month": self.add_this_month])) } } - @Published var day_boundary: Int{ + @Published var day_boundary: Int { didSet { self.updatePlaylist(updates: JSON(["day_boundary": self.day_boundary])) } } - @Published var chart_range: LastFmRange{ + @Published var chart_range: LastFmRange { didSet { self.updatePlaylist(updates: JSON(["chart_range": self.chart_range.rawValue])) } } - @Published var chart_limit: Int{ + @Published var chart_limit: Int { didSet { - self.updatePlaylist(updates: JSON(["chart_range": self.chart_range.rawValue])) + self.updatePlaylist(updates: JSON(["chart_limit": self.chart_limit])) } } func updatePlaylist(updates: JSON) { - let api = PlaylistApi.updatePlaylist(name: self.name, updates: updates) - RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in - switch response.response?.statusCode { - case 200, 201: - break - case _: - Logger.net.error("error: \(self.name), \(updates)") + let api = PlaylistApi.updatePlaylist(name: self.name, updates: updates) + RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in + switch response.response?.statusCode { + case 200, 201: + break + case _: + Logger.net.error("error: \(self.name), \(updates)") + } } + //TODO: do better error checking } - //TODO: do better error checking - } private enum CodingKeys: String, CodingKey { case name diff --git a/Mixonomer/Model/User.swift b/Mixonomer/Model/User.swift index 5028a4e..885a1ec 100644 --- a/Mixonomer/Model/User.swift +++ b/Mixonomer/Model/User.swift @@ -15,7 +15,7 @@ enum UserType: String, Decodable { case admin = "admin" } -class User: Identifiable, Decodable { +class User: Identifiable, Decodable, ObservableObject { //MARK: Properties @@ -30,7 +30,29 @@ class User: Identifiable, Decodable { var last_refreshed: String var spotify_linked: Bool - @Published var lastfm_username: String? { + + @Published var notify: Bool { + didSet { + self.updateUser(updates: JSON(["notify": self.notify])) + } + } + @Published var notify_playlist_updates: Bool { + didSet { + self.updateUser(updates: JSON(["notify_playlist_updates": self.notify_playlist_updates])) + } + } + @Published var notify_tag_updates: Bool { + didSet { + self.updateUser(updates: JSON(["notify_tag_updates": self.notify_tag_updates])) + } + } + @Published var notify_admins: Bool { + didSet { + self.updateUser(updates: JSON(["notify_admins": self.notify_admins])) + } + } + + @Published var lastfm_username: String { didSet { self.updateUser(updates: JSON(["lastfm_username": self.lastfm_username])) } @@ -48,7 +70,12 @@ class User: Identifiable, Decodable { last_keygen: String = "", last_refreshed: String = "", spotify_linked: Bool = true, - lastfm_username: String? = nil){ + lastfm_username: String = "", + + notify: Bool = false, + notify_playlist_updates: Bool = false, + notify_tag_updates: Bool = false, + notify_admins: Bool = false){ self.username = username self.email = email @@ -61,19 +88,21 @@ class User: Identifiable, Decodable { self.last_refreshed = last_refreshed self.spotify_linked = spotify_linked self.lastfm_username = lastfm_username + + self.notify = notify + self.notify_playlist_updates = notify_playlist_updates + self.notify_tag_updates = notify_tag_updates + self.notify_admins = notify_admins } - + func updateUser(updates: JSON) { let api = UserApi.updateUser(updates: updates) RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in - switch response.response?.statusCode { - case 200, 201: - break - case _: + + if !NetworkHelper.check_network_response(response: response) { Logger.net.error("error while updating user: \(updates)") } } - //TODO: do better error checking } private enum CodingKeys: String, CodingKey { @@ -89,6 +118,11 @@ class User: Identifiable, Decodable { case spotify_linked case lastfm_username + + case notify + case notify_playlist_updates + case notify_tag_updates + case notify_admins } required init(from decoder: Decoder) throws { @@ -120,7 +154,31 @@ class User: Identifiable, Decodable { do{ lastfm_username = try container.decode(String.self, forKey: .lastfm_username) }catch { - lastfm_username = nil + lastfm_username = "" + } + + do{ + notify = try container.decode(Bool.self, forKey: .notify) + }catch { + notify = false + } + + do{ + notify_playlist_updates = try container.decode(Bool.self, forKey: .notify_playlist_updates) + }catch { + notify_playlist_updates = false + } + + do{ + notify_tag_updates = try container.decode(Bool.self, forKey: .notify_tag_updates) + }catch { + notify_tag_updates = false + } + + do{ + notify_admins = try container.decode(Bool.self, forKey: .notify_admins) + }catch { + notify_admins = false } } @@ -139,6 +197,15 @@ class User: Identifiable, Decodable { try container.encode(self.spotify_linked, forKey: .spotify_linked) try container.encode(self.lastfm_username, forKey: .lastfm_username) + + try container.encode(self.notify, forKey: .notify) + try container.encode(self.notify_playlist_updates, forKey: .notify_playlist_updates) + try container.encode(self.notify_tag_updates, forKey: .notify_tag_updates) + try container.encode(self.notify_admins, forKey: .notify_admins) + } + + static func get_null_user() -> User { + return User() } } diff --git a/Mixonomer/Network/NetworkHelper.swift b/Mixonomer/Network/NetworkHelper.swift new file mode 100644 index 0000000..1c95eb4 --- /dev/null +++ b/Mixonomer/Network/NetworkHelper.swift @@ -0,0 +1,38 @@ +// +// NetworkHelper.swift +// Mixonomer +// +// Created by Andy Pack on 27/11/2022. +// Copyright © 2022 Sarsoo. All rights reserved. +// + +import Foundation +import OSLog +import Alamofire + +class NetworkHelper { + + static func check_network_response(response: AFDataResponse, onTokenFail: (() -> Void)? = nil) -> Bool { + + if let statusCode = response.response?.statusCode { + switch statusCode { + case 401: // token has expired + Logger.sys.info("token expired, logging user out") + onTokenFail?() + return false + case 400..<500: + Logger.net.error("client fault \(statusCode)") + return false + case 500..<600: + Logger.net.warning("server fault \(statusCode)") + return false + case _: // 200 -> Success + return true + } + } + + Logger.net.error("failed to access network status code to validate") + + return false + } +} diff --git a/Mixonomer/Network/UserApi.swift b/Mixonomer/Network/UserApi.swift index 153c0ec..43e4e69 100644 --- a/Mixonomer/Network/UserApi.swift +++ b/Mixonomer/Network/UserApi.swift @@ -15,6 +15,11 @@ public enum UserApi { case getUser case updateUser(updates: JSON) case deleteUser + case passAPNSToken(updates: String) + case updateNotify(state: Bool) + case updateNotifyPlaylist(state: String) + case updateNotifyTag(state: String) + case updateNotifyAdmin(state: String) } extension UserApi: ApiRequest { @@ -30,6 +35,10 @@ extension UserApi: ApiRequest { return "api/user" case .deleteUser: return "api/user" + case .passAPNSToken: + return "api/user" + case .updateNotify, .updateNotifyPlaylist, .updateNotifyTag, .updateNotifyAdmin: + return "api/user" } } @@ -41,6 +50,10 @@ extension UserApi: ApiRequest { return .post case .deleteUser: return .delete + case .passAPNSToken: + return .post + case .updateNotify, .updateNotifyPlaylist, .updateNotifyTag, .updateNotifyAdmin: + return .post } } @@ -50,6 +63,16 @@ extension UserApi: ApiRequest { return nil case .updateUser(let updates): return updates + case .passAPNSToken(let token): + return JSON(["apns_token": token]) + case .updateNotify(let state): + return JSON(["notify": state]) + case .updateNotifyPlaylist(let state): + return JSON(["notify_playlist_updates": state]) + case .updateNotifyTag(let state): + return JSON(["notify_tag_updates": state]) + case .updateNotifyAdmin(let state): + return JSON(["notify_admins": state]) } } @@ -57,7 +80,9 @@ extension UserApi: ApiRequest { switch self { case .getUser, .deleteUser: return nil - case .updateUser: + case .updateUser, .passAPNSToken: + return JSONParameterEncoder.default + case .updateNotify, .updateNotifyPlaylist, .updateNotifyTag, .updateNotifyAdmin: return JSONParameterEncoder.default } } @@ -70,7 +95,7 @@ extension UserApi: ApiRequest { return ApiRequestDefaults.authMethod } - static func fromJSON(user: Data) -> User? { + static func fromJSON(user: Data) -> User { let decoder = JSONDecoder() do { @@ -78,11 +103,11 @@ extension UserApi: ApiRequest { return user } catch { Logger.parse.error("error parsing user from json: \(error)") + return User.get_null_user() } - return nil } - static func fromJSON(user: JSON) -> User? { + static func fromJSON(user: JSON) -> User { let _json = user.rawString()?.data(using: .utf8) @@ -95,16 +120,16 @@ extension UserApi: ApiRequest { Logger.parse.error("error parsing user from json: \(error)") } } - return nil + + return User.get_null_user() } static func fromJSON(user: [JSON]) -> [User] { var _users: [User] = [] for dict in user { let _iter = self.fromJSON(user: dict) - if let returned = _iter { - _users.append(returned) - } + + _users.append(_iter) } return _users } diff --git a/Mixonomer/Notifications/APNSHandler.swift b/Mixonomer/Notifications/APNSHandler.swift new file mode 100644 index 0000000..750a178 --- /dev/null +++ b/Mixonomer/Notifications/APNSHandler.swift @@ -0,0 +1,37 @@ +// +// APNSHandler.swift +// Mixonomer +// +// Created by Andy Pack on 27/11/2022. +// Copyright © 2022 Sarsoo. All rights reserved. +// + +import Foundation +import OSLog + +class APNSHandler { + + static func pass_token_to_backend(onFailure: (() -> Void)? = nil) { + // check if a token is waiting to be handed off + if let token = StaticNotif.token { + if !StaticNotif.hasDelivered { + + Logger.sys.info("passing off APNS network token") + let api = UserApi.passAPNSToken(updates: token) + RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in + + if NetworkHelper.check_network_response(response: response) { + Logger.net.debug("successfully handed off APNS token") + StaticNotif.hasDelivered = true + } else { + Logger.net.error("failed to hand off APNS token: \(response.response?.statusCode ?? 0)") + onFailure?() + } + } + } + } + else { + Logger.sys.debug("no APNS token waiting, skipping network handoff") + } + } +} diff --git a/Mixonomer/Notifications/StaticNotif.swift b/Mixonomer/Notifications/StaticNotif.swift new file mode 100644 index 0000000..c1aa748 --- /dev/null +++ b/Mixonomer/Notifications/StaticNotif.swift @@ -0,0 +1,14 @@ +// +// StaticNotif.swift +// Mixonomer +// +// Created by Andy Pack on 27/11/2022. +// Copyright © 2022 Sarsoo. All rights reserved. +// + +import Foundation + +class StaticNotif { + static var token: String? + static var hasDelivered: Bool = false +} diff --git a/Mixonomer/Views/Admin/UsersList.swift b/Mixonomer/Views/Admin/UsersList.swift index f3a7cfa..00cff9e 100644 --- a/Mixonomer/Views/Admin/UsersList.swift +++ b/Mixonomer/Views/Admin/UsersList.swift @@ -74,6 +74,6 @@ struct UsersList: View { struct UsersList_Previews: PreviewProvider { static var previews: some View { UsersList() - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/AppSkeleton.swift b/Mixonomer/Views/AppSkeleton.swift index 9da79c4..8dc4548 100644 --- a/Mixonomer/Views/AppSkeleton.swift +++ b/Mixonomer/Views/AppSkeleton.swift @@ -77,6 +77,6 @@ struct AppSkeleton: View { struct RootView_Previews: PreviewProvider { static var previews: some View { AppSkeleton() - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/LoginScreen.swift b/Mixonomer/Views/LoginScreen.swift index 788327d..0bc36c7 100644 --- a/Mixonomer/Views/LoginScreen.swift +++ b/Mixonomer/Views/LoginScreen.swift @@ -237,11 +237,11 @@ struct LoginScreen_Previews: PreviewProvider { static var previews: some View { Group{ LoginScreen(screenMode: .None) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) LoginScreen(screenMode: .Login) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) LoginScreen(screenMode: .Register) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } } diff --git a/Mixonomer/Views/Playlist/AddPlaylistSheet.swift b/Mixonomer/Views/Playlist/AddPlaylistSheet.swift index 2bd4043..37048e5 100644 --- a/Mixonomer/Views/Playlist/AddPlaylistSheet.swift +++ b/Mixonomer/Views/Playlist/AddPlaylistSheet.swift @@ -112,6 +112,6 @@ struct AddPlaylistSheet: View { struct AddPlaylistSheet_Previews: PreviewProvider { static var previews: some View { AddPlaylistSheet(playlists: .constant([]), username: .constant("username")) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/Playlist/PlaylistList.swift b/Mixonomer/Views/Playlist/PlaylistList.swift index ed5e08c..1628c62 100644 --- a/Mixonomer/Views/Playlist/PlaylistList.swift +++ b/Mixonomer/Views/Playlist/PlaylistList.swift @@ -22,7 +22,7 @@ struct PlaylistList: View { NavigationView { List{ - if liveUser.user?.spotify_linked == false { + if liveUser.user.spotify_linked == false { Text("Spotify isn't linked, login to the web client to pair") Button(action: { @@ -101,9 +101,9 @@ struct PlaylistList_Previews: PreviewProvider { static var previews: some View { Group { PlaylistList() - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) PlaylistList() - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false, user: User())) + .environmentObject(LiveUser.get_preview_user_with_user()) } } } diff --git a/Mixonomer/Views/Playlist/PlaylistRow.swift b/Mixonomer/Views/Playlist/PlaylistRow.swift index b3942ef..4c08ffd 100644 --- a/Mixonomer/Views/Playlist/PlaylistRow.swift +++ b/Mixonomer/Views/Playlist/PlaylistRow.swift @@ -63,6 +63,6 @@ struct PlaylistRow_Previews: PreviewProvider { PlaylistView(playlist: .constant( Playlist(name: "playlist name", username: "username") )) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/Playlist/PlaylistView.swift b/Mixonomer/Views/Playlist/PlaylistView.swift index 32a2668..9f5eee3 100644 --- a/Mixonomer/Views/Playlist/PlaylistView.swift +++ b/Mixonomer/Views/Playlist/PlaylistView.swift @@ -366,7 +366,7 @@ struct PlaylistView_Previews: PreviewProvider { lastfm_stat_artist_percent: 80 ) )) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) PlaylistView(playlist: .constant( Playlist(name: "playlist name", username: "username", @@ -375,7 +375,7 @@ struct PlaylistView_Previews: PreviewProvider { lastfm_stat_artist_percent: 80 ) )) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false, user: User())) + .environmentObject(LiveUser.get_preview_user_with_user()) } } diff --git a/Mixonomer/Views/Settings/NotificationsControls.swift b/Mixonomer/Views/Settings/NotificationsControls.swift new file mode 100644 index 0000000..0966b41 --- /dev/null +++ b/Mixonomer/Views/Settings/NotificationsControls.swift @@ -0,0 +1,52 @@ +// +// NotificationsControls.swift +// Mixonomer +// +// Created by Andy Pack on 27/11/2022. +// Copyright © 2022 Sarsoo. All rights reserved. +// + +import SwiftUI + +struct NotificationsControls: View { + + @EnvironmentObject var liveUser: LiveUser + + var body: some View { + List { + Section { + Toggle(isOn: self.$liveUser.user.notify) { + Text("Enabled") + } + } + Section { + Button("Request permission on this device") { + self.liveUser.requestAPNSPerms() + } + } + Section { + Toggle(isOn: self.$liveUser.user.notify_playlist_updates) { + Text("Playlist Updates") + } + Toggle(isOn: self.$liveUser.user.notify_tag_updates) { + Text("Tag Updates") + } + + if liveUser.user.type == .admin { + Toggle(isOn: self.$liveUser.user.notify_admins) { + Text("Admin Updates") + } + } + } + } + .listStyle(GroupedListStyle()) + .navigationBarTitle(Text("Notifications 🔔")) + } +} + +struct NotificationsControls_Previews: PreviewProvider { + static var previews: some View { + NotificationsControls() + .environmentObject(LiveUser.get_preview_user()) + } +} diff --git a/Mixonomer/Views/Settings/SettingsList.swift b/Mixonomer/Views/Settings/SettingsList.swift index 73599ad..0cdb57c 100644 --- a/Mixonomer/Views/Settings/SettingsList.swift +++ b/Mixonomer/Views/Settings/SettingsList.swift @@ -16,13 +16,7 @@ struct SettingsList: View { @State private var deleteAlertShowing = false var body: some View { - - let spotify_link_bind = Binding(get: { liveUser.user?.spotify_linked ?? false}, - set: { newVal in liveUser.user?.spotify_linked = newVal }) -// let lastfm_bind = Binding(get: { liveUser.user?.lastfm_username ?? ""}, -// set: { newVal in liveUser.user?.lastfm_username = newVal }) - - return NavigationView { + NavigationView { List{ Section { Button(action: { @@ -47,21 +41,25 @@ struct SettingsList: View { } Section(header: Text("Integrations")) { - Toggle(isOn: spotify_link_bind) { + Toggle(isOn: self.$liveUser.user.spotify_linked) { Text("Spotify Link") } .disabled(true) -// NavigationLink("Last.fm Username") { -// List{ -// TextField("Username", text: lastfm_bind) -// } -// } + NavigationLink("Last.fm") { + List{ + TextField("Username", text: self.$liveUser.user.lastfm_username) + } + } } -// Section(header: Text("Last.fm")) { -// TextField("Last.fm Username", text: lastfm_bind) -// } + Section { + NavigationLink(destination: NotificationsControls()) { + HStack { + Text("Notifications") + } + } + } Section { Button(action: { @@ -128,6 +126,6 @@ struct SettingsList: View { struct SettingsList_Previews: PreviewProvider { static var previews: some View { SettingsList() - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/Tag/AddTagSheet.swift b/Mixonomer/Views/Tag/AddTagSheet.swift index 99611e0..23713f5 100644 --- a/Mixonomer/Views/Tag/AddTagSheet.swift +++ b/Mixonomer/Views/Tag/AddTagSheet.swift @@ -94,6 +94,6 @@ struct AddTagSheet: View { struct AddTagSheet_Previews: PreviewProvider { static var previews: some View { AddTagSheet(tags: .constant([]), username: .constant("username")) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/Tag/TagList.swift b/Mixonomer/Views/Tag/TagList.swift index 0c27fff..ecfe18c 100644 --- a/Mixonomer/Views/Tag/TagList.swift +++ b/Mixonomer/Views/Tag/TagList.swift @@ -93,6 +93,6 @@ struct TagList: View { struct TagList_Previews: PreviewProvider { static var previews: some View { TagList() - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/Tag/TagRow.swift b/Mixonomer/Views/Tag/TagRow.swift index 6b8cef0..c6af955 100644 --- a/Mixonomer/Views/Tag/TagRow.swift +++ b/Mixonomer/Views/Tag/TagRow.swift @@ -66,6 +66,6 @@ struct TagRow_Previews: PreviewProvider { last_updated: "10th Feb") )) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } } diff --git a/Mixonomer/Views/Tag/TagView.swift b/Mixonomer/Views/Tag/TagView.swift index 48cc21f..2865360 100644 --- a/Mixonomer/Views/Tag/TagView.swift +++ b/Mixonomer/Views/Tag/TagView.swift @@ -118,32 +118,41 @@ struct TagView: View { } func runTag() { + + Logger.net.debug("running tag from view: \(self.tag.name)") + let api = TagApi.runTag(tag_id: tag.tag_id) RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in if self.liveUser.check_network_response(response: response) { - + Logger.net.debug("successfully running tag: \(self.tag.name)") } else { - + Logger.net.error("request failed for running tag: \(self.tag.name)") } } //TODO: do better error checking } func updateTag(updates: JSON) { + + Logger.net.debug("updating tag from view: \(self.tag.name)") + let api = TagApi.updateTag(tag_id: tag.tag_id, updates: updates) RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in if self.liveUser.check_network_response(response: response) { - + Logger.net.debug("successfully updated tag: \(self.tag.name)") } else { - + Logger.net.error("request failed for updating tag: \(self.tag.name)") } } //TODO: do better error checking } func refreshTag() { + + Logger.net.debug("refreshing tag from view: \(self.tag.name)") + let api = TagApi.getTag(tag_id: self.tag.tag_id) RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in @@ -190,6 +199,6 @@ struct TagView_Previews: PreviewProvider { last_updated: "10th Feb") )) - .environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) + .environmentObject(LiveUser.get_preview_user()) } }