The code paths for generating zap notifications were very different from the paths used by most other notifications. In this commit, I include the logic and data structures necessary for formatting zap notifications in the same fashion as local notifications. A good amount of refactoring and moving functions/structures around was necessary to reuse zap local notification logic. I also attempted to make the notification generation process more consistent between zaps and other notifications, without changing too much of existing logic to avoid even more regression risk. General push notifications + local notifications test ----------------------------------------------------- PASS Device: iPhone 15 Pro simulator iOS: 17.0.1 Damus: This commit Setup: - Two phones running Damus on different accounts - Local relay with strfry-push-notify test setup - Apple push notification test tool Coverage: 1. Mention notifications 2. DM notifications 3. Reaction notifications 4. Repost notifications Steps for each notification type: 1. Trigger a notification (local and then push) 2. Ensure that the notification is received on the other device 3. Ensure that the notification is formatted correctly 4. Ensure that DMs are decrypted correctly 5. Ensure that profile names are unfurled correctly 6. Click on the notification and ensure that the app opens to the correct screen Result: PASS (all notifications received and formatted correctly) Notes: - For some reason my relay is not receiving zap events, so I could not test zap notifications yet. - Reply notifications do not seem to be implemented yet - These apply to the tests below as well Changelog-Added: Zap notification support for push notifications Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Signed-off-by: William Casarin <jb55@jb55.com>
156 lines
4.7 KiB
Swift
156 lines
4.7 KiB
Swift
//
|
|
// WalletConnect.swift
|
|
// damus
|
|
//
|
|
// Created by William Casarin on 2023-03-22.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct WalletConnectURL: Equatable {
|
|
static func == (lhs: WalletConnectURL, rhs: WalletConnectURL) -> Bool {
|
|
return lhs.keypair == rhs.keypair &&
|
|
lhs.pubkey == rhs.pubkey &&
|
|
lhs.relay == rhs.relay
|
|
}
|
|
|
|
let relay: RelayURL
|
|
let keypair: FullKeypair
|
|
let pubkey: Pubkey
|
|
let lud16: String?
|
|
|
|
func to_url() -> URL {
|
|
var urlComponents = URLComponents()
|
|
urlComponents.scheme = "nostrwalletconnect"
|
|
urlComponents.host = pubkey.hex()
|
|
urlComponents.queryItems = [
|
|
URLQueryItem(name: "relay", value: relay.id),
|
|
URLQueryItem(name: "secret", value: keypair.privkey.hex())
|
|
]
|
|
|
|
if let lud16 {
|
|
urlComponents.queryItems?.append(URLQueryItem(name: "lud16", value: lud16))
|
|
}
|
|
|
|
return urlComponents.url!
|
|
}
|
|
|
|
init?(str: String) {
|
|
guard let components = URLComponents(string: str),
|
|
components.scheme == "nostrwalletconnect" || components.scheme == "nostr+walletconnect",
|
|
// The line below provides flexibility for both `nostrwalletconnect://` (non-compliant, but commonly used) and `nostrwalletconnect:` (NIP-47 compliant) formats
|
|
let encoded_pubkey = components.path == "" ? components.host : components.path,
|
|
let pubkey = hex_decode_pubkey(encoded_pubkey),
|
|
let items = components.queryItems,
|
|
let relay = items.first(where: { qi in qi.name == "relay" })?.value,
|
|
let relay_url = RelayURL(relay),
|
|
let secret = items.first(where: { qi in qi.name == "secret" })?.value,
|
|
secret.utf8.count == 64,
|
|
let decoded = hex_decode(secret)
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
let privkey = Privkey(Data(decoded))
|
|
guard let our_pk = privkey_to_pubkey(privkey: privkey) else { return nil }
|
|
|
|
let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value
|
|
let keypair = FullKeypair(pubkey: our_pk, privkey: privkey)
|
|
self = WalletConnectURL(pubkey: pubkey, relay: relay_url, keypair: keypair, lud16: lud16)
|
|
}
|
|
|
|
init(pubkey: Pubkey, relay: RelayURL, keypair: FullKeypair, lud16: String?) {
|
|
self.pubkey = pubkey
|
|
self.relay = relay
|
|
self.keypair = keypair
|
|
self.lud16 = lud16
|
|
}
|
|
}
|
|
|
|
struct WalletRequest<T: Codable>: Codable {
|
|
let method: String
|
|
let params: T?
|
|
}
|
|
|
|
struct WalletResponseErr: Codable {
|
|
let code: String?
|
|
let message: String?
|
|
}
|
|
|
|
struct PayInvoiceResponse: Decodable {
|
|
let preimage: String
|
|
}
|
|
|
|
enum WalletResponseResultType: String {
|
|
case pay_invoice
|
|
}
|
|
|
|
enum WalletResponseResult {
|
|
case pay_invoice(PayInvoiceResponse)
|
|
}
|
|
|
|
struct FullWalletResponse {
|
|
let req_id: NoteId
|
|
let response: WalletResponse
|
|
|
|
init?(from: NostrEvent, nwc: WalletConnectURL) async {
|
|
guard let note_id = from.referenced_ids.first else {
|
|
return nil
|
|
}
|
|
|
|
self.req_id = note_id
|
|
|
|
let ares = Task {
|
|
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
|
|
let resp: WalletResponse = decode_json(json)
|
|
else {
|
|
let resp: WalletResponse? = nil
|
|
return resp
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
guard let res = await ares.value else {
|
|
return nil
|
|
}
|
|
|
|
self.response = res
|
|
}
|
|
|
|
}
|
|
|
|
struct WalletResponse: Decodable {
|
|
let result_type: WalletResponseResultType
|
|
let error: WalletResponseErr?
|
|
let result: WalletResponseResult?
|
|
|
|
private enum CodingKeys: CodingKey {
|
|
case result_type, error, result
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let result_type_str = try container.decode(String.self, forKey: .result_type)
|
|
|
|
guard let result_type = WalletResponseResultType(rawValue: result_type_str) else {
|
|
throw DecodingError.typeMismatch(WalletResponseResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown"))
|
|
}
|
|
|
|
self.result_type = result_type
|
|
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
|
|
|
|
guard self.error == nil else {
|
|
self.result = nil
|
|
return
|
|
}
|
|
|
|
switch result_type {
|
|
case .pay_invoice:
|
|
let res = try container.decode(PayInvoiceResponse.self, forKey: .result)
|
|
self.result = .pay_invoice(res)
|
|
}
|
|
}
|
|
}
|
|
|