Push Notifications test

This commit is contained in:
lumaa-dev 2024-11-30 16:28:40 +01:00
parent 3818ad9fe3
commit ff034e6591
8 changed files with 351 additions and 6 deletions

View File

@ -150,6 +150,7 @@
B9A80E9B2C67D56900DE3D88 /* FollowGoalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */; };
B9A80E9C2C67D56900DE3D88 /* CreatePostWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A80DDD2C67BFF800DE3D88 /* CreatePostWidget.swift */; };
B9A8DABA2BB7364300A890CC /* PostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A8DAB92BB7364300A890CC /* PostsView.swift */; };
B9AC6BD12CF341C2009C65C7 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = B9AC6BD02CF341C2009C65C7 /* Secret.plist */; };
B9B469B02B9A275F00AD5585 /* FollowGoalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */; };
B9B469B22B9A6E8300AD5585 /* PrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469B12B9A6E8300AD5585 /* PrivacyView.swift */; };
B9B469DB2B9B2EDB00AD5585 /* ComingSoonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469DA2B9B2EDB00AD5585 /* ComingSoonView.swift */; };
@ -185,6 +186,8 @@
B9D173E22CA0555800CB575F /* MetricsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D173E12CA0555800CB575F /* MetricsManager.swift */; };
B9D173E32CA0555800CB575F /* MetricsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D173E12CA0555800CB575F /* MetricsManager.swift */; };
B9D365612B79A1BE004C1255 /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D365602B79A1BE004C1255 /* MailView.swift */; };
B9D700722CF295AB00A6DB81 /* AppNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D700712CF295AB00A6DB81 /* AppNotification.swift */; };
B9D700732CF295AB00A6DB81 /* AppNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D700712CF295AB00A6DB81 /* AppNotification.swift */; };
B9D9C6C12B6A56E000C26A41 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C02B6A56E000C26A41 /* Notification.swift */; };
B9D9C6C32B6A576C00C26A41 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C22B6A576C00C26A41 /* NotificationsView.swift */; };
B9D9C6C52B6A587700C26A41 /* NotificationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C42B6A587700C26A41 /* NotificationRow.swift */; };
@ -317,6 +320,7 @@
B9A80DDD2C67BFF800DE3D88 /* CreatePostWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePostWidget.swift; sourceTree = "<group>"; };
B9A80DE12C67C38E00DE3D88 /* URLNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLNavigator.swift; sourceTree = "<group>"; };
B9A8DAB92BB7364300A890CC /* PostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsView.swift; sourceTree = "<group>"; };
B9AC6BD02CF341C2009C65C7 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Secret.plist; sourceTree = "<group>"; };
B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowGoalWidget.swift; sourceTree = "<group>"; };
B9B469B12B9A6E8300AD5585 /* PrivacyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyView.swift; sourceTree = "<group>"; };
B9B469DA2B9B2EDB00AD5585 /* ComingSoonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComingSoonView.swift; sourceTree = "<group>"; };
@ -348,6 +352,7 @@
B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchStoryboard.storyboard; sourceTree = "<group>"; };
B9D173E12CA0555800CB575F /* MetricsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsManager.swift; sourceTree = "<group>"; };
B9D365602B79A1BE004C1255 /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
B9D700712CF295AB00A6DB81 /* AppNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotification.swift; sourceTree = "<group>"; };
B9D9C6C02B6A56E000C26A41 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
B9D9C6C22B6A576C00C26A41 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
B9D9C6C42B6A587700C26A41 /* NotificationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRow.swift; sourceTree = "<group>"; };
@ -548,6 +553,7 @@
B9D9C6BF2B6A56D500C26A41 /* Notifications */ = {
isa = PBXGroup;
children = (
B9D700712CF295AB00A6DB81 /* AppNotification.swift */,
B9D9C6C02B6A56E000C26A41 /* Notification.swift */,
B999DE5D2B76F9D100509868 /* Message.swift */,
);
@ -592,6 +598,7 @@
children = (
B9C20D592B923D53004DC9B3 /* Bubble.entitlements */,
B9FB94A02B2EF23100D81C07 /* Info.plist */,
B9AC6BD02CF341C2009C65C7 /* Secret.plist */,
B9FB945A2B2DEECE00D81C07 /* BubbleApp.swift */,
B9EBE8572B474FD600FB594D /* AppDelegate.swift */,
B9FB946E2B2DF3BB00D81C07 /* Components */,
@ -875,6 +882,7 @@
files = (
B97BCE282B3ED2A80044756D /* .gitignore in Resources */,
B9CDE7AD2C9FF536004B1BDD /* PolySans-BulkyWide.ttf in Resources */,
B9AC6BD12CF341C2009C65C7 /* Secret.plist in Resources */,
B9DC69302B79378400E625B9 /* BubblePlus.storekit in Resources */,
B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */,
B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */,
@ -917,6 +925,7 @@
B9A80E272C67C4B700DE3D88 /* PostDetailsView.swift in Sources */,
B9A80E282C67C4B700DE3D88 /* IconView.swift in Sources */,
B9A80E292C67C4B700DE3D88 /* UpdateView.swift in Sources */,
B9D700722CF295AB00A6DB81 /* AppNotification.swift in Sources */,
B9A80E2A2C67C4B700DE3D88 /* ReportStatusView.swift in Sources */,
B9A80E2B2C67C4B700DE3D88 /* PrivacyView.swift in Sources */,
B9A80E2C2C67C4B700DE3D88 /* SafariView.swift in Sources */,
@ -1067,6 +1076,7 @@
B9A80E9A2C67D56900DE3D88 /* FollowCountWidget.swift in Sources */,
B9A80E9B2C67D56900DE3D88 /* FollowGoalWidget.swift in Sources */,
B9A80E9C2C67D56900DE3D88 /* CreatePostWidget.swift in Sources */,
B9D700732CF295AB00A6DB81 /* AppNotification.swift in Sources */,
B9C7F46C2C387D3B009C36DC /* WarningView.swift in Sources */,
B9EBE8562B47256900FB594D /* PostAttachment.swift in Sources */,
B9EBE8582B474FD600FB594D /* AppDelegate.swift in Sources */,

View File

@ -1,18 +1,21 @@
//Made by Lumaa
import SwiftUI
import SwiftData
import UIKit
import RevenueCat
import UserNotifications
@Observable
public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicationDelegate {
public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicationDelegate, UNUserNotificationCenterDelegate {
public var window: UIWindow?
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
public private(set) var secret: [String: String] = [:]
public static var premium: Bool = false
public static var tokenized: Bool = false
public func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
window = windowScene.keyWindow
@ -20,7 +23,31 @@ public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicati
override public init() {
super.init()
AppNotification.requestAuthorization { success in
guard !Self.tokenized else { return }
Self.tokenized = true
let ownedAccs: [LoggedAccount] = self.getAccounts()
ownedAccs.forEach { acc in
Task {
let tempCli: Client = .init(server: acc.app?.server ?? "mastodon.social", oauthToken: acc.token)
await AppNotification.sendToken(client: tempCli, oauth: acc.token)
}
}
}
#if !WIDGET
if !UIApplication.shared.isRegisteredForRemoteNotifications {
print("Registering REMOTE NOTIFICATION")
Task {
UIApplication.shared.registerForRemoteNotifications()
}
} else {
let token: String? = UserDefaults.standard.string(forKey: "deviceToken")
print("ALREADY registered REMOTE NOTIFICATION \(token ?? "???")")
}
#endif
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
@ -39,7 +66,21 @@ public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicati
Self.observedSceneDelegate.insert(self)
_ = Self.observer // just for activating the lazy static property
}
public static var deviceToken: String = "[X]"
public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
AppDelegate.deviceToken = token
UserDefaults.standard.setValue(token, forKey: "deviceToken")
print("[TOKEN] Got deviceToken: \(token)")
// send device token to server
}
public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
print("[TOKEN]: \(error)")
}
static func readSecret() -> [String: String]? {
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let url = URL(fileURLWithPath: path)
@ -51,7 +92,15 @@ public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicati
return nil
}
private func getAccounts() -> [LoggedAccount] {
guard let modelContainer: ModelContainer = try? ModelContainer(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: false)) else { return [] }
let modelContext = ModelContext(modelContainer)
let loggedAccounts = try? modelContext.fetch(FetchDescriptor<LoggedAccount>())
return loggedAccounts ?? []
}
/// This function uses the REAL customer info to access the premium state
// static func hasPlus(completionHandler: @escaping (Bool) -> Void) {
// Purchases.shared.getCustomerInfo { (customerInfo, error) in

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.lumaa.ThreadedApp</string>

View File

@ -6,6 +6,8 @@ import RevenueCat
@main
struct BubbleApp: App {
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
init() {
BubbleShortcuts.updateAppShortcutParameters() //might not work?

View File

@ -0,0 +1,121 @@
// Made by Lumaa
import UIKit
import SwiftUI
import Foundation
import UserNotifications
class AppNotification {
/// Authorizes the app to send the device's APNS token to the "Push\_URL" string in the `Secret.plist` file
private static var registerToken: Bool = false
@AppStorage("sentToken") private static var sentToken: Bool = false
static var hasSentToken: Bool {
get {
self.sentToken
}
}
static var allowedNotifications: Bool = false
init() {}
static func requestAuthorization(completionHandler: @escaping (Bool) -> Void = {_ in}) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, err in
completionHandler(success)
self.allowedNotifications = success
guard success else { print("Did not validate"); return }
print("REQUESTED PUSH NOTIFICATION")
Task {
#if !WIDGET
await UIApplication.shared.registerForRemoteNotifications()
#endif
}
}
}
static func sendToken(client: Client, oauth: OauthToken) async {
guard let acc: Account = try? await client.get(endpoint: Accounts.verifyCredentials), let accUrl: URL = acc.url, let server: String = accUrl.host() else { return }
self.sendToken(instanceUrl: server, accessToken: oauth.accessToken)
}
static func sendToken(account: Account, oauth: OauthToken) {
guard let server = account.acct.split(separator: "@").first else { return }
self.sendToken(instanceUrl: String(server), accessToken: oauth.accessToken)
}
static private func sendToken(instanceUrl: String, accessToken: String) {
guard let plist = AppDelegate.readSecret(), let baseUrl = plist["Push_URL"], !Self.hasSentToken, Self.allowedNotifications && Self.registerToken else {
return
}
let header: [String: String] = [
"deviceToken": AppDelegate.deviceToken,
"instance": instanceUrl,
"accessToken": accessToken
]
var formatted: String = ""
for (key, value) in header {
if value == header.values.reversed().first {
formatted += "\(key)=\(value)"
} else {
formatted += "\(key)=\(value)&"
}
}
print(formatted)
// let encoder: JSONEncoder = .init()
// if let json = try? encoder.encode(header) {
// var req = URLRequest(url: URL(string: "\(baseUrl)/push/add")!)
// req.httpMethod = "POST"
//// req.addValue("application/json", forHTTPHeaderField: "Content-Type")
// req.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
// req.httpBody = json
//
// URLSession.shared.dataTask(with: req) { data, res, err in
// if err != nil {
// print(err?.localizedDescription ?? "No error details")
// return
// }
//
// let decoder: JSONDecoder = .init()
// if let genRes: GenericResponse = try? decoder.decode(GenericResponse.self, from: data ?? Data()) {
// let resType: String = genRes.success ? "successfully" : "incorrectly" // incorrect cause idk how to say "failed" with -ly
// print("Server \(resType) replied with: \(genRes.message ?? "[NO MESSAGE]")")
// } else {
// print("No valid GenericResponse was output: \(String(data: data ?? Data(), encoding: .utf8) ?? "[NOT DECODED]")")
// }
// }.resume()
// }
var req = URLRequest(url: URL(string: "\(baseUrl)/push/add")!, timeoutInterval: Double.infinity)
req.httpMethod = "POST"
req.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
req.httpBody = formatted.data(using: .utf8)
URLSession.shared.dataTask(with: req) { data, res, err in
if err != nil {
print(err?.localizedDescription ?? "No error details")
return
}
let decoder: JSONDecoder = .init()
if let genRes: GenericResponse = try? decoder.decode(GenericResponse.self, from: data ?? Data()) {
let resType: String = genRes.success ? "successfully" : "incorrectly" // incorrect cause idk how to say "failed" with -ly
print("Server \(resType) replied with: \(genRes.message ?? "[NO MESSAGE]")")
} else {
print("No valid GenericResponse was output: \(String(data: data ?? Data(), encoding: .utf8) ?? "[NOT DECODED]")")
}
}.resume()
}
/// This is a generic server response from the APNS server
struct GenericResponse: Decodable {
let success: Bool
let message: String?
}
}

View File

@ -5,6 +5,27 @@ import Foundation
public struct Notification: Decodable, Identifiable, Equatable {
public enum NotificationType: String, CaseIterable {
case follow, follow_request, mention, reblog, status, favourite, poll, update
var localizedPush: String {
switch self {
case .follow:
String(localized: "push.follow", comment: "PUSH NOTIFICATION \\w BODY")
case .follow_request:
String(localized: "push.follow_request", comment: "PUSH NOTIFICATION \\w BODY")
case .mention:
String(localized: "push.mention", comment: "PUSH NOTIFICATION \\w BODY")
case .reblog:
String(localized: "push.reblog", comment: "PUSH NOTIFICATION \\w BODY")
case .status:
String(localized: "push.status", comment: "PUSH NOTIFICATION \\w BODY")
case .favourite:
String(localized: "push.favourite", comment: "PUSH NOTIFICATION \\w BODY")
case .poll:
String(localized: "push.poll", comment: "PUSH NOTIFICATION \\w BODY")
case .update:
String(localized: "push.update", comment: "PUSH NOTIFICATION")
}
}
}
public let id: String

View File

@ -21,5 +21,9 @@
<array>
<string>PolySans-BulkyWide.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
</dict>
</plist>

View File

@ -2320,6 +2320,142 @@
}
}
},
"push.favourite" : {
"comment" : "PUSH NOTIFICATION \\w BODY",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "liked your post: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "a aimé votre publication : %@"
}
}
}
},
"push.follow" : {
"comment" : "PUSH NOTIFICATION \\w BODY",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "followed you: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "vous a suivi : %@"
}
}
}
},
"push.follow_request" : {
"comment" : "PUSH NOTIFICATION \\w BODY",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "wants to follow you: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "veut vous suivre : %@"
}
}
}
},
"push.mention" : {
"comment" : "PUSH NOTIFICATION \\w BODY",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "mentioned you: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "vous a mentionné : %@"
}
}
}
},
"push.poll" : {
"comment" : "PUSH NOTIFICATION \\w BODY",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Poll ended: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vote terminé : %@"
}
}
}
},
"push.reblog" : {
"comment" : "PUSH NOTIFICATION \\w BODY",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "reblogged your post: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "a reposté votre publication : %@"
}
}
}
},
"push.status" : {
"comment" : "PUSH NOTIFICATION \\w BODY",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "posted: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "a posté : %@"
}
}
}
},
"push.update" : {
"comment" : "PUSH NOTIFICATION",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Update? Idk…"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mise à jour ? Jsp…"
}
}
}
},
"restricted.blocked-domain" : {
"extractionState" : "manual",
"localizations" : {
@ -5337,4 +5473,4 @@
}
},
"version" : "1.0"
}
}