Swift notification plugin (#436)

* Add Swift notification plugin

* Hash the metadata

* Validate min sendable amount

* Remove initializer as base class, UNNotificationServiceExtension, has no default initializer

* Set the PaymentMethod

* Handle PaymentDetails in SwapUpdated

* Improve payment text
This commit is contained in:
Ross Savage
2024-08-30 09:20:13 +02:00
committed by GitHub
parent 0053007000
commit b493f3dc03
12 changed files with 600 additions and 0 deletions

View File

@@ -90,6 +90,10 @@ jobs:
zip -9 -r breez_sdk_liquidFFI.xcframework.zip breez_sdk_liquidFFI.xcframework
echo "XCF_CHECKSUM=`swift package compute-checksum breez_sdk_liquidFFI.xcframework.zip`" >> $GITHUB_ENV
- name: Remove dist Sources
working-directory: dist
run: git rm -r Sources
- name: Update Swift Package definition
working-directory: build/lib/bindings/langs/swift
run: |

View File

@@ -0,0 +1,50 @@
import Foundation
import os.log
#if DEBUG && true
fileprivate var logger = OSLog(
subsystem: Bundle.main.bundleIdentifier!,
category: "BreezSDKLiquidConnector"
)
#else
fileprivate var logger = OSLog.disabled
#endif
class BreezSDKLiquidConnector {
private static var liquidSDK: BindingLiquidSdk? = nil
fileprivate static var queue = DispatchQueue(label: "BreezSDKLiquidConnector")
fileprivate static var sdkListener: EventListener? = nil
static func register(connectRequest: ConnectRequest, listener: EventListener) throws -> BindingLiquidSdk? {
try BreezSDKLiquidConnector.queue.sync { [] in
BreezSDKLiquidConnector.sdkListener = listener
if BreezSDKLiquidConnector.liquidSDK == nil {
BreezSDKLiquidConnector.liquidSDK = try BreezSDKLiquidConnector.connectSDK(connectRequest: connectRequest)
}
return BreezSDKLiquidConnector.liquidSDK
}
}
static func unregister() {
BreezSDKLiquidConnector.queue.sync { [] in
BreezSDKLiquidConnector.sdkListener = nil
}
}
static func connectSDK(connectRequest: ConnectRequest) throws -> BindingLiquidSdk? {
// Connect to the Breez Liquid SDK make it ready for use
os_log("Connecting to Breez Liquid SDK", log: logger, type: .debug)
let liquidSDK = try connect(req: connectRequest)
os_log("Connected to Breez Liquid SDK", log: logger, type: .debug)
let _ = try liquidSDK.addEventListener(listener: BreezSDKEventListener())
return liquidSDK
}
}
class BreezSDKEventListener: EventListener {
func onEvent(e: SdkEvent) {
BreezSDKLiquidConnector.queue.async { [] in
BreezSDKLiquidConnector.sdkListener?.onEvent(e: e)
}
}
}

View File

@@ -0,0 +1,35 @@
import Foundation
struct Constants {
// Notification Threads
static let NOTIFICATION_THREAD_LNURL_PAY = "LNURL_PAY"
static let NOTIFICATION_THREAD_SWAP_UPDATED = "SWAP_UPDATED"
// Message Data
static let MESSAGE_DATA_TYPE = "notification_type"
static let MESSAGE_DATA_PAYLOAD = "notification_payload"
static let MESSAGE_TYPE_SWAP_UPDATED = "swap_updated"
static let MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info"
static let MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice"
// Resource Identifiers
static let LNURL_PAY_INFO_NOTIFICATION_TITLE = "lnurl_pay_info_notification_title"
static let LNURL_PAY_INVOICE_NOTIFICATION_TITLE = "lnurl_pay_invoice_notification_title"
static let LNURL_PAY_METADATA_PLAIN_TEXT = "lnurl_pay_metadata_plain_text"
static let LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "lnurl_pay_notification_failure_title"
static let PAYMENT_RECEIVED_NOTIFICATION_TITLE = "payment_received_notification_title"
static let PAYMENT_SENT_NOTIFICATION_TITLE = "payment_sent_notification_title"
static let SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "swap_confirmed_notification_failure_text"
static let SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "swap_confirmed_notification_failure_title"
// Resource Identifier Defaults
static let DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE = "Retrieving Payment Information"
static let DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE = "Fetching Invoice"
static let DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT = "Pay with LNURL"
static let DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "Receive Payment Failed"
static let DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE = "Received %d sats"
static let DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE = "Sent %d sats"
static let DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "Tap to complete payment"
static let DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "Payment Pending"
}

View File

@@ -0,0 +1,19 @@
import Foundation
public class ResourceHelper {
public static let shared = ResourceHelper()
private init() {/* must use shared instance */}
public func getString(key: String, fallback: String) -> String {
return getString(key: key, validateContains: nil, fallback: fallback)
}
public func getString(key: String, validateContains: String?, fallback: String) -> String {
if let str = Bundle.main.object(forInfoDictionaryKey: key) as? String {
if validateContains == nil || str.contains(validateContains!) {
return str
}
}
return fallback
}}

View File

@@ -0,0 +1,94 @@
import UserNotifications
import os.log
open class SDKNotificationService: UNNotificationServiceExtension {
fileprivate let TAG = "SDKNotificationService"
var liquidSDK: BindingLiquidSdk?
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var currentTask: TaskProtocol?
public var logger: ServiceLogger = ServiceLogger(logStream: nil)
override public init() { }
override open func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.logger.log(tag: TAG, line: "Notification received", level: "INFO")
self.contentHandler = contentHandler
self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let connectRequest = self.getConnectRequest() else {
if let content = bestAttemptContent {
contentHandler(content)
}
return
}
if let currentTask = self.getTaskFromNotification() {
self.currentTask = currentTask
DispatchQueue.main.async { [self] in
do {
logger.log(tag: TAG, line: "Breez Liquid SDK is not connected, connecting...", level: "INFO")
liquidSDK = try BreezSDKLiquidConnector.register(connectRequest: connectRequest, listener: currentTask)
logger.log(tag: TAG, line: "Breez Liquid SDK connected successfully", level: "INFO")
try currentTask.start(liquidSDK: liquidSDK!)
} catch {
logger.log(tag: TAG, line: "Breez Liquid SDK connection failed \(error)", level: "ERROR")
shutdown()
}
}
}
}
open func getConnectRequest() -> ConnectRequest? {
return nil
}
open func getTaskFromNotification() -> TaskProtocol? {
guard let content = bestAttemptContent else { return nil }
guard let notificationType = content.userInfo[Constants.MESSAGE_DATA_TYPE] as? String else { return nil }
self.logger.log(tag: TAG, line: "Notification payload: \(content.userInfo)", level: "INFO")
self.logger.log(tag: TAG, line: "Notification type: \(notificationType)", level: "INFO")
guard let payload = content.userInfo[Constants.MESSAGE_DATA_PAYLOAD] as? String else {
contentHandler!(content)
return nil
}
self.logger.log(tag: TAG, line: "\(notificationType) data string: \(payload)", level: "INFO")
switch(notificationType) {
case Constants.MESSAGE_TYPE_SWAP_UPDATED:
return SwapUpdatedTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
case Constants.MESSAGE_TYPE_LNURL_PAY_INFO:
return LnurlPayInfoTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
case Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE:
return LnurlPayInvoiceTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
default:
return nil
}
}
override open func serviceExtensionTimeWillExpire() {
self.logger.log(tag: TAG, line: "serviceExtensionTimeWillExpire()", level: "INFO")
// iOS calls this function just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content,
// otherwise the original push payload will be used.
self.shutdown()
}
private func shutdown() -> Void {
self.logger.log(tag: TAG, line: "shutting down...", level: "INFO")
BreezSDKLiquidConnector.unregister()
self.logger.log(tag: TAG, line: "task unregistered", level: "INFO")
self.currentTask?.onShutdown()
}
public func setLogger(logger: Logger) {
self.logger = ServiceLogger(logStream: logger)
}
}

View File

@@ -0,0 +1,40 @@
import Foundation
import os.log
#if DEBUG && true
fileprivate var logger = OSLog(
subsystem: Bundle.main.bundleIdentifier!,
category: "ServiceLogger"
)
#else
fileprivate var logger = OSLog.disabled
#endif
open class ServiceLogger {
var logStream: Logger?
init(logStream: Logger?) {
self.logStream = logStream
}
public func log(tag: String, line: String, level: String) {
if let logger = logStream {
logger.log(l: LogEntry(line: line, level: level))
} else {
switch(level) {
case "ERROR":
os_log("[%{public}@] %{public}@", log: logger, type: .error, tag, line)
break
case "INFO", "WARN":
os_log("[%{public}@] %{public}@", log: logger, type: .info, tag, line)
break
case "TRACE":
os_log("[%{public}@] %{public}@", log: logger, type: .debug, tag, line)
break
default:
os_log("[%{public}@] %{public}@", log: logger, type: .default, tag, line)
return
}
}
}
}

View File

@@ -0,0 +1,36 @@
import Foundation
import CommonCrypto
extension Data {
public func sha256() -> String {
return hexStringFromData(input: digest(input: self as NSData))
}
private func digest(input : NSData) -> NSData {
let digestLength = Int(CC_SHA256_DIGEST_LENGTH)
var hash = [UInt8](repeating: 0, count: digestLength)
CC_SHA256(input.bytes, UInt32(input.length), &hash)
return NSData(bytes: hash, length: digestLength)
}
private func hexStringFromData(input: NSData) -> String {
var bytes = [UInt8](repeating: 0, count: input.length)
input.getBytes(&bytes, length: input.length)
var hexString = ""
for byte in bytes {
hexString += String(format:"%02x", UInt8(byte))
}
return hexString
}
}
public extension String {
func sha256() -> String {
if let stringData = self.data(using: String.Encoding.utf8) {
return stringData.sha256()
}
return ""
}
}

View File

@@ -0,0 +1,89 @@
import UserNotifications
import Foundation
struct LnurlErrorResponse: Decodable, Encodable {
let status: String
let reason: String
init(status: String, reason: String) {
self.status = status
self.reason = reason
}
}
class LnurlPayTask : TaskProtocol {
var payload: String
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var logger: ServiceLogger
var successNotificationTitle: String
var failNotificationTitle: String
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil, successNotificationTitle: String, failNotificationTitle: String) {
self.payload = payload
self.contentHandler = contentHandler
self.bestAttemptContent = bestAttemptContent
self.logger = logger
self.successNotificationTitle = successNotificationTitle;
self.failNotificationTitle = failNotificationTitle;
}
func start(liquidSDK: BindingLiquidSdk) throws {}
public func onEvent(e: SdkEvent) {}
func onShutdown() {
displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
}
func replyServer(encodable: Encodable, replyURL: String) {
guard let serverReplyURL = URL(string: replyURL) else {
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
return
}
var request = URLRequest(url: serverReplyURL)
request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(encodable)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200 {
self.displayPushNotification(title: self.successNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
} else {
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
return
}
}
task.resume()
}
func fail(withError: String, replyURL: String, failNotificationTitle: String? = nil) {
if let serverReplyURL = URL(string: replyURL) {
var request = URLRequest(url: serverReplyURL)
request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(LnurlErrorResponse(status: "ERROR", reason: withError))
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let _ = (response as! HTTPURLResponse).statusCode
}
task.resume()
}
let title = failNotificationTitle != nil ? failNotificationTitle! : self.failNotificationTitle
self.displayPushNotification(title: title, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
}
}
enum InvalidLnurlPayError: Error {
case minSendable
case amount(amount: UInt64)
}
extension InvalidLnurlPayError: LocalizedError {
public var errorDescription: String? {
switch self {
case .minSendable:
return NSLocalizedString("Minimum sendable amount is invalid", comment: "InvalidLnurlPayError")
case .amount(amount: let amount):
return NSLocalizedString("Invalid amount requested \(amount)", comment: "InvalidLnurlPayError")
}
}
}

View File

@@ -0,0 +1,64 @@
import UserNotifications
import Foundation
struct LnurlInfoRequest: Codable {
let callback_url: String
let reply_url: String
}
struct LnurlInfoResponse: Decodable, Encodable {
let callback: String
let maxSendable: UInt64
let minSendable: UInt64
let metadata: String
let tag: String
init(callback: String, maxSendable: UInt64, minSendable: UInt64, metadata: String, tag: String) {
self.callback = callback
self.maxSendable = maxSendable
self.minSendable = minSendable
self.metadata = metadata
self.tag = tag
}
}
class LnurlPayInfoTask : LnurlPayTask {
fileprivate let TAG = "LnurlPayInfoTask"
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
let successNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_INFO_NOTIFICATION_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE)
let failNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE)
super.init(payload: payload, logger: logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent, successNotificationTitle: successNotificationTitle, failNotificationTitle: failNotificationTitle)
}
override func start(liquidSDK: BindingLiquidSdk) throws {
var request: LnurlInfoRequest? = nil
do {
request = try JSONDecoder().decode(LnurlInfoRequest.self, from: self.payload.data(using: .utf8)!)
} catch let e {
self.logger.log(tag: TAG, line: "failed to decode payload: \(e)", level: "ERROR")
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
throw e
}
do {
// Get the lightning limits
let limits = try liquidSDK.fetchLightningLimits()
// Max millisatoshi amount LN SERVICE is willing to receive
let maxSendableMsat = limits.receive.maxSat * UInt64(1000)
// Min millisatoshi amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendableMsat`
let minSendableMsat = limits.receive.minSat * UInt64(1000)
if minSendableMsat < UInt64(1) || minSendableMsat > maxSendableMsat {
throw InvalidLnurlPayError.minSendable
}
// Format the response
let plainTextMetadata = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_METADATA_PLAIN_TEXT, fallback: Constants.DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT)
let metadata = "[[\"text/plain\",\"\(plainTextMetadata)\"]]"
replyServer(encodable: LnurlInfoResponse(callback: request!.callback_url, maxSendable: maxSendableMsat, minSendable: minSendableMsat, metadata: metadata, tag: "payRequest"),
replyURL: request!.reply_url)
} catch let e {
self.logger.log(tag: TAG, line: "failed to process lnurl: \(e)", level: "ERROR")
fail(withError: e.localizedDescription, replyURL: request!.reply_url)
}
}
}

View File

@@ -0,0 +1,56 @@
import UserNotifications
import Foundation
struct LnurlInvoiceRequest: Codable {
let reply_url: String
let amount: UInt64
}
struct LnurlInvoiceResponse: Decodable, Encodable {
let pr: String
let routes: [String]
init(pr: String, routes: [String]) {
self.pr = pr
self.routes = routes
}
}
class LnurlPayInvoiceTask : LnurlPayTask {
fileprivate let TAG = "LnurlPayInvoiceTask"
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
let successNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_INVOICE_NOTIFICATION_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE)
let failNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE)
super.init(payload: payload, logger: logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent, successNotificationTitle: successNotificationTitle, failNotificationTitle: failNotificationTitle)
}
override func start(liquidSDK: BindingLiquidSdk) throws {
var request: LnurlInvoiceRequest? = nil
do {
request = try JSONDecoder().decode(LnurlInvoiceRequest.self, from: self.payload.data(using: .utf8)!)
} catch let e {
self.logger.log(tag: TAG, line: "failed to decode payload: \(e)", level: "ERROR")
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
throw e
}
do {
// Get the lightning limits
let limits = try liquidSDK.fetchLightningLimits()
// Check amount is within limits
let amountSat = request!.amount / UInt64(1000)
if amountSat < limits.receive.minSat || amountSat > limits.receive.maxSat {
throw InvalidLnurlPayError.amount(amount: request!.amount)
}
let plainTextMetadata = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_METADATA_PLAIN_TEXT, fallback: Constants.DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT)
let metadata = "[[\"text/plain\",\"\(plainTextMetadata)\"]]"
let prepareReceivePaymentRes = try liquidSDK.prepareReceivePayment(req: PrepareReceiveRequest(payerAmountSat: amountSat, paymentMethod: PaymentMethod.lightning))
let receivePaymentRes = try liquidSDK.receivePayment(req: ReceivePaymentRequest(prepareResponse: prepareReceivePaymentRes, description: metadata, useDescriptionHash: true))
self.replyServer(encodable: LnurlInvoiceResponse(pr: receivePaymentRes.destination, routes: []), replyURL: request!.reply_url)
} catch let e {
self.logger.log(tag: TAG, line: "failed to process lnurl: \(e)", level: "ERROR")
self.fail(withError: e.localizedDescription, replyURL: request!.reply_url)
}
}
}

View File

@@ -0,0 +1,80 @@
import UserNotifications
import Foundation
struct SwapUpdatedRequest: Codable {
let id: String
let status: String
}
class SwapUpdatedTask : TaskProtocol {
fileprivate let TAG = "SwapUpdatedTask"
internal var payload: String
internal var contentHandler: ((UNNotificationContent) -> Void)?
internal var bestAttemptContent: UNMutableNotificationContent?
internal var logger: ServiceLogger
internal var request: SwapUpdatedRequest? = nil
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
self.payload = payload
self.contentHandler = contentHandler
self.bestAttemptContent = bestAttemptContent
self.logger = logger
}
func start(liquidSDK: BindingLiquidSdk) throws {
do {
self.request = try JSONDecoder().decode(SwapUpdatedRequest.self, from: self.payload.data(using: .utf8)!)
} catch let e {
self.logger.log(tag: TAG, line: "Failed to decode payload: \(e)", level: "ERROR")
self.onShutdown()
throw e
}
}
public func onEvent(e: SdkEvent) {
if let swapIdHash = self.request?.id {
switch e {
case .paymentSucceeded(details: let payment):
let swapId = self.getSwapId(details: payment.details)
if swapIdHash == swapId?.sha256() {
self.logger.log(tag: TAG, line: "Received payment succeeded event: \(swapIdHash)", level: "INFO")
self.notifySuccess(payment: payment)
}
break
default:
break
}
}
}
func getSwapId(details: PaymentDetails?) -> String? {
if let details = details {
switch details {
case let .bitcoin(swapId, _, _, _):
return swapId
case let .lightning(swapId, _, _, _, _, _):
return swapId
default:
break
}
}
return nil
}
func onShutdown() {
let notificationTitle = ResourceHelper.shared.getString(key: Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE)
let notificationBody = ResourceHelper.shared.getString(key: Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT, fallback: Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT)
self.displayPushNotification(title: notificationTitle, body: notificationBody, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED)
}
func notifySuccess(payment: Payment) {
self.logger.log(tag: TAG, line: "Payment \(payment.txId ?? "") completed successfully", level: "INFO")
let received = payment.paymentType == PaymentType.receive
let notificationTitle = ResourceHelper.shared.getString(
key: received ? Constants.PAYMENT_RECEIVED_NOTIFICATION_TITLE : Constants.PAYMENT_SENT_NOTIFICATION_TITLE,
validateContains: "%d",
fallback: received ? Constants.DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE: Constants.DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE)
self.displayPushNotification(title: String(format: notificationTitle, payment.amountSat), logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED)
}
}

View File

@@ -0,0 +1,33 @@
import UserNotifications
public protocol TaskProtocol : EventListener {
var payload: String { get set }
var contentHandler: ((UNNotificationContent) -> Void)? { get set }
var bestAttemptContent: UNMutableNotificationContent? { get set }
func start(liquidSDK: BindingLiquidSdk) throws
func onShutdown()
}
extension TaskProtocol {
func displayPushNotification(title: String, body: String? = nil, logger: ServiceLogger, threadIdentifier: String? = nil) {
logger.log(tag: "TaskProtocol", line:"displayPushNotification \(title)", level: "INFO")
guard
let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent
else {
return
}
if let body = body {
bestAttemptContent.body = body
}
if let threadIdentifier = threadIdentifier {
bestAttemptContent.threadIdentifier = threadIdentifier
}
bestAttemptContent.title = title
contentHandler(bestAttemptContent)
}
}