Add some support for signer requests
This commit is contained in:
@@ -436,6 +436,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Yeti/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Yeti/Preview Content\"";
|
||||||
|
DEVELOPMENT_TEAM = S99A5B637C;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -466,6 +467,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Yeti/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Yeti/Preview Content\"";
|
||||||
|
DEVELOPMENT_TEAM = S99A5B637C;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import SwiftData
|
|||||||
|
|
||||||
@Model
|
@Model
|
||||||
final class NostrClientModel {
|
final class NostrClientModel {
|
||||||
@Attribute(.unique) var id: String
|
@Attribute(.unique) var name: String
|
||||||
|
|
||||||
@Relationship(deleteRule: .cascade) var signEventPermissions: [SignEventPermissionModel] = []
|
@Relationship(deleteRule: .cascade) var signEventPermissions: [SignEventPermissionModel] = []
|
||||||
|
|
||||||
var signingPolicy: SigningPolicy?
|
|
||||||
|
|
||||||
var readPublicKeyPermission: Bool = false
|
var readPublicKeyPermission: Bool = false
|
||||||
var nip04EncryptPermission: Bool = false
|
var nip04EncryptPermission: Bool = false
|
||||||
var nip44EncryptPermission: Bool = false
|
var nip44EncryptPermission: Bool = false
|
||||||
@@ -24,7 +22,7 @@ final class NostrClientModel {
|
|||||||
var getRelaysPermission: Bool = false
|
var getRelaysPermission: Bool = false
|
||||||
var decryptZapEventPermission: Bool = false
|
var decryptZapEventPermission: Bool = false
|
||||||
|
|
||||||
init(id: String) {
|
init(name: String) {
|
||||||
self.id = id
|
self.name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,3 +19,11 @@ final class ProfileSettingsModel {
|
|||||||
self.publicKey = publicKey
|
self.publicKey = publicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileSettingsModel {
|
||||||
|
static func predicateByPublicKey(_ publicKey: String) -> Predicate<ProfileSettingsModel> {
|
||||||
|
#Predicate<ProfileSettingsModel> { profileSettingsModel in
|
||||||
|
profileSettingsModel.publicKey == publicKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import SwiftData
|
|||||||
@Model
|
@Model
|
||||||
final class SignEventPermissionModel {
|
final class SignEventPermissionModel {
|
||||||
var kind: Int
|
var kind: Int
|
||||||
var allowed: Bool
|
var approved: Bool
|
||||||
|
|
||||||
init(kind: Int, allowed: Bool) {
|
init(kind: Int, approved: Bool) {
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.allowed = allowed
|
self.approved = approved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
68
Yeti/Models/SignerRequestModel.swift
Normal file
68
Yeti/Models/SignerRequestModel.swift
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// SignerRequestModel.swift
|
||||||
|
// Yeti
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 1/22/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
@Model
|
||||||
|
final class SignerRequestModel {
|
||||||
|
var type: NostrSignerType
|
||||||
|
var returnType: NostrSignerReturnType
|
||||||
|
var compressionType: NostrSignerCompressionType
|
||||||
|
var createdAt: Date
|
||||||
|
var callbackURL: String?
|
||||||
|
var pubkey: String?
|
||||||
|
var sourceApplication: String?
|
||||||
|
var targetApplication: String?
|
||||||
|
var approved: Bool?
|
||||||
|
var decidedAt: Date?
|
||||||
|
|
||||||
|
init(
|
||||||
|
type: NostrSignerType,
|
||||||
|
returnType: NostrSignerReturnType,
|
||||||
|
compressionType: NostrSignerCompressionType,
|
||||||
|
createdAt: Date,
|
||||||
|
callbackURL: String? = nil,
|
||||||
|
pubkey: String? = nil,
|
||||||
|
sourceApplication: String? = nil,
|
||||||
|
targetApplication: String? = nil,
|
||||||
|
approved: Bool? = nil,
|
||||||
|
decidedAt: Date? = nil
|
||||||
|
) {
|
||||||
|
self.type = type
|
||||||
|
self.returnType = returnType
|
||||||
|
self.compressionType = compressionType
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.callbackURL = callbackURL
|
||||||
|
self.pubkey = pubkey
|
||||||
|
self.sourceApplication = sourceApplication
|
||||||
|
self.targetApplication = targetApplication
|
||||||
|
self.approved = approved
|
||||||
|
self.decidedAt = decidedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NostrSignerType: String, Codable, CaseIterable {
|
||||||
|
case getPublicKey = "get_public_key"
|
||||||
|
case signEvent = "sign_event"
|
||||||
|
case nip04Encrypt = "nip04_encrypt"
|
||||||
|
case nip44Encrypt = "nip44_encrypt"
|
||||||
|
case nip04Decrypt = "nip04_decrypt"
|
||||||
|
case nip44Decrypt = "nip44_decrypt"
|
||||||
|
case getRelays = "get_relays"
|
||||||
|
case decryptZapEvent = "decrypt_zap_event"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NostrSignerReturnType: String, Codable {
|
||||||
|
case signature
|
||||||
|
case event
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NostrSignerCompressionType: String, Codable {
|
||||||
|
case none
|
||||||
|
case gzip
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum SigningPolicy: Int, Codable, CaseIterable {
|
enum SigningPolicy: Codable, CaseIterable {
|
||||||
case basic = 0
|
case basic
|
||||||
case manual = 1
|
case manual
|
||||||
|
|
||||||
var name: String {
|
var name: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
|||||||
@@ -7,9 +7,15 @@
|
|||||||
"Add your key" : {
|
"Add your key" : {
|
||||||
"comment" : "Title of view to add the user’s private key."
|
"comment" : "Title of view to add the user’s private key."
|
||||||
},
|
},
|
||||||
|
"Allow" : {
|
||||||
|
"comment" : "Button to allow a permission request."
|
||||||
|
},
|
||||||
"Approve basic actions" : {
|
"Approve basic actions" : {
|
||||||
"comment" : "Name of event signing policy that approves basic actions."
|
"comment" : "Name of event signing policy that approves basic actions."
|
||||||
},
|
},
|
||||||
|
"Approved: %@" : {
|
||||||
|
"comment" : "Text indicating that a signer request is either approved or not approved."
|
||||||
|
},
|
||||||
"Create a key" : {
|
"Create a key" : {
|
||||||
"comment" : "Button to create a key."
|
"comment" : "Button to create a key."
|
||||||
},
|
},
|
||||||
@@ -22,6 +28,9 @@
|
|||||||
"Decrypt zap events" : {
|
"Decrypt zap events" : {
|
||||||
"comment" : "Permission toggle to allow Nostr client to decrypt zap events."
|
"comment" : "Permission toggle to allow Nostr client to decrypt zap events."
|
||||||
},
|
},
|
||||||
|
"Deny" : {
|
||||||
|
"comment" : "Button to deny a permission request."
|
||||||
|
},
|
||||||
"Done" : {
|
"Done" : {
|
||||||
"comment" : "Button to go to the next view that adds the user’s entered private key."
|
"comment" : "Button to go to the next view that adds the user’s entered private key."
|
||||||
},
|
},
|
||||||
@@ -49,9 +58,15 @@
|
|||||||
"Next" : {
|
"Next" : {
|
||||||
"comment" : "Button to go to the next view that adds the user’s entered private key."
|
"comment" : "Button to go to the next view that adds the user’s entered private key."
|
||||||
},
|
},
|
||||||
|
"Nothing to approve yet" : {
|
||||||
|
"comment" : "Text on Home tab to indicate that there are no permission requests to review for approval."
|
||||||
|
},
|
||||||
"nsec / private key" : {
|
"nsec / private key" : {
|
||||||
"comment" : "Prompt asking user to enter in a Nostr private key."
|
"comment" : "Prompt asking user to enter in a Nostr private key."
|
||||||
},
|
},
|
||||||
|
"Pending" : {
|
||||||
|
"comment" : "Text indicating that a signer request is pending approval."
|
||||||
|
},
|
||||||
"Permissions" : {
|
"Permissions" : {
|
||||||
"comment" : "Title for Permissions tab"
|
"comment" : "Title for Permissions tab"
|
||||||
},
|
},
|
||||||
@@ -76,6 +91,9 @@
|
|||||||
"Sign kind %@ events" : {
|
"Sign kind %@ events" : {
|
||||||
"comment" : "Permission toggle to allow Nostr client to sign events of a specific kind.."
|
"comment" : "Permission toggle to allow Nostr client to sign events of a specific kind.."
|
||||||
},
|
},
|
||||||
|
"Why not explore your favorite Nostr app a bit?" : {
|
||||||
|
"comment" : "Text on Home tab to suggest what the user could do instead\nsince there are no permission requests to review for approval."
|
||||||
|
},
|
||||||
"Yeti: Nostr Helper" : {
|
"Yeti: Nostr Helper" : {
|
||||||
"comment" : "Application title."
|
"comment" : "Application title."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,9 +6,29 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
|
class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
|
||||||
|
private static let nostrSignerURLScheme = "nostrsigner"
|
||||||
|
|
||||||
|
private let modelContainer: ModelContainer
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
let schema = Schema([
|
||||||
|
GeneralSettingsModel.self,
|
||||||
|
ProfileSettingsModel.self,
|
||||||
|
SignerRequestModel.self
|
||||||
|
])
|
||||||
|
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||||
|
|
||||||
|
do {
|
||||||
|
modelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||||
|
} catch {
|
||||||
|
fatalError("Could not create ModelContainer: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func scene(
|
func scene(
|
||||||
_ scene: UIScene,
|
_ scene: UIScene,
|
||||||
willConnectTo session: UISceneSession,
|
willConnectTo session: UISceneSession,
|
||||||
@@ -26,32 +46,29 @@ class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func scene(_ scene: UIScene, openURLContext: UIOpenURLContext) {
|
private func scene(_ scene: UIScene, openURLContext: UIOpenURLContext) {
|
||||||
let sendingAppID = openURLContext.options.sourceApplication
|
var generalSettingsModelDescriptor = FetchDescriptor<GeneralSettingsModel>()
|
||||||
let url = openURLContext.url
|
generalSettingsModelDescriptor.fetchLimit = 1
|
||||||
|
|
||||||
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
|
guard let generalSettingsModel = try? modelContainer.mainContext.fetch(generalSettingsModelDescriptor).first,
|
||||||
components.scheme == "nostrsigner",
|
let activePublicKey = generalSettingsModel.activePublicKey
|
||||||
let queryItems = components.queryItems else {
|
else {
|
||||||
print("Invalid URL or path missing")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("source application = \(sendingAppID ?? "Unknown")")
|
let sourceApplication = openURLContext.options.sourceApplication
|
||||||
print("url = \(url)")
|
let url = openURLContext.url
|
||||||
|
|
||||||
if let path = components.path {
|
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
print("path = \(path)")
|
components.scheme == SceneDelegate.nostrSignerURLScheme,
|
||||||
|
let queryItems = components.queryItems else {
|
||||||
|
print("Invalid URL or path missing. \(url.absoluteString)")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if let host = components.host {
|
|
||||||
print("host = \(host)")
|
|
||||||
}
|
|
||||||
|
|
||||||
print("queryItems = \(queryItems)")
|
|
||||||
|
|
||||||
let params = queryItems.reduce(into: [:]) { $0[$1.name] = $1.value }
|
let params = queryItems.reduce(into: [:]) { $0[$1.name] = $1.value }
|
||||||
|
|
||||||
let compressionType: NostrSignerCompressionType
|
let compressionType: NostrSignerCompressionType
|
||||||
if let rawCompressionType = params["compressionType"] {
|
if let rawCompressionType = params[NostrSignerURLQueryItemName.compressionType.rawValue] {
|
||||||
if let maybeCompressionType = NostrSignerCompressionType(rawValue: rawCompressionType) {
|
if let maybeCompressionType = NostrSignerCompressionType(rawValue: rawCompressionType) {
|
||||||
compressionType = maybeCompressionType
|
compressionType = maybeCompressionType
|
||||||
} else {
|
} else {
|
||||||
@@ -61,44 +78,52 @@ class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
|
|||||||
compressionType = .none
|
compressionType = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let rawType = params["type"],
|
guard let rawType = params[NostrSignerURLQueryItemName.type.rawValue],
|
||||||
let type = NostrSignerType(rawValue: rawType),
|
let type = NostrSignerType(rawValue: rawType),
|
||||||
let rawReturnType = params["returnType"],
|
let rawReturnType = params[NostrSignerURLQueryItemName.returnType.rawValue],
|
||||||
let returnType = NostrSignerReturnType(rawValue: rawReturnType) else {
|
let returnType = NostrSignerReturnType(rawValue: rawReturnType)
|
||||||
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("type = \(type)")
|
let callbackURLString = params[NostrSignerURLQueryItemName.callbackURL.rawValue]
|
||||||
print("returnType = \(returnType)")
|
let targetApplication = targetApplication(callbackURLString)
|
||||||
print("compressionType = \(compressionType)")
|
|
||||||
|
|
||||||
if let pubkey = params["pubkey"] {
|
let pubkey = params[NostrSignerURLQueryItemName.pubkey.rawValue]
|
||||||
print("pubkey = \(pubkey)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let callbackURL = params["callbackUrl"] {
|
let signerRequestModel = SignerRequestModel(
|
||||||
print("callbackURL = \(callbackURL)")
|
type: type,
|
||||||
|
returnType: returnType,
|
||||||
|
compressionType: compressionType,
|
||||||
|
createdAt: Date.now,
|
||||||
|
callbackURL: callbackURLString,
|
||||||
|
pubkey: pubkey,
|
||||||
|
sourceApplication: sourceApplication,
|
||||||
|
targetApplication: targetApplication
|
||||||
|
)
|
||||||
|
|
||||||
|
modelContainer.mainContext.insert(signerRequestModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func targetApplication(_ callbackURLString: String?) -> String? {
|
||||||
|
if let callbackURLString, let callbackURL = URL(string: callbackURLString) {
|
||||||
|
if let scheme = callbackURL.scheme {
|
||||||
|
let lowercasedScheme = scheme.lowercased()
|
||||||
|
if lowercasedScheme == "http" || lowercasedScheme == "https" {
|
||||||
|
return callbackURL.host()
|
||||||
|
} else {
|
||||||
|
return lowercasedScheme
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NostrSignerType: String, CaseIterable {
|
private enum NostrSignerURLQueryItemName: String {
|
||||||
case getPublicKey = "get_public_key"
|
case callbackURL = "callbackUrl"
|
||||||
case signEvent = "sign_event"
|
case compressionType
|
||||||
case nip04Encrypt = "nip04_encrypt"
|
case pubkey
|
||||||
case nip44Encrypt = "nip44_encrypt"
|
case returnType
|
||||||
case nip04Decrypt = "nip04_decrypt"
|
case type
|
||||||
case nip44Decrypt = "nip44_decrypt"
|
|
||||||
case getRelays = "get_relays"
|
|
||||||
case decryptZapEvent = "decrypt_zap_event"
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NostrSignerReturnType: String, CaseIterable {
|
|
||||||
case signature
|
|
||||||
case event
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NostrSignerCompressionType: String, CaseIterable {
|
|
||||||
case none
|
|
||||||
case gzip
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ struct ContentView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if generalSettingsModels.first!.activePublicKey == nil {
|
if let activePublicKey = generalSettingsModels.first?.activePublicKey {
|
||||||
OnboardingView()
|
LoggedInView(publicKey: activePublicKey)
|
||||||
} else {
|
} else {
|
||||||
LoggedInView()
|
OnboardingView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,5 @@ struct ContentView: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentView()
|
ContentView()
|
||||||
.modelContainer(for: ProfileSettingsModel.self, inMemory: true)
|
.modelContainer(for: GeneralSettingsModel.self, inMemory: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,40 @@
|
|||||||
// Created by Terry Yiu on 1/20/25.
|
// Created by Terry Yiu on 1/20/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import SwiftData
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HistoryView: View {
|
struct HistoryView: View {
|
||||||
|
@Query(sort: \SignerRequestModel.createdAt, order: .reverse) private var signerRequestModels: [SignerRequestModel]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
EmptyView()
|
List {
|
||||||
|
ForEach(signerRequestModels, id: \.self) { signerRequestModel in
|
||||||
|
Section {
|
||||||
|
VStack {
|
||||||
|
Text(signerRequestModel.type.rawValue)
|
||||||
|
Text(signerRequestModel.returnType.rawValue)
|
||||||
|
Text(signerRequestModel.compressionType.rawValue)
|
||||||
|
Text(signerRequestModel.createdAt.description)
|
||||||
|
Text(signerRequestModel.callbackURL ?? "Unknown callbackURL")
|
||||||
|
Text(signerRequestModel.pubkey ?? "Unknown pubkey")
|
||||||
|
Text(signerRequestModel.sourceApplication ?? "Unknown sourceApplication")
|
||||||
|
Text(signerRequestModel.targetApplication ?? "Unknown targetApplication")
|
||||||
|
|
||||||
|
switch signerRequestModel.approved {
|
||||||
|
case .none:
|
||||||
|
Text("Pending", comment: "Text indicating that a signer request is pending approval.")
|
||||||
|
case .some(let approved):
|
||||||
|
Text(
|
||||||
|
"Approved: \(approved.description)",
|
||||||
|
comment: "Text indicating that a signer request is either approved or not approved.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(signerRequestModel.decidedAt?.description ?? "Unknown decisionTimestamp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,72 @@
|
|||||||
// Created by Terry Yiu on 1/20/25.
|
// Created by Terry Yiu on 1/20/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import SwiftData
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
|
@Query(
|
||||||
|
filter: #Predicate<SignerRequestModel> { signerRequestModel in
|
||||||
|
signerRequestModel.approved == nil
|
||||||
|
},
|
||||||
|
sort: \SignerRequestModel.createdAt, order: .reverse
|
||||||
|
)
|
||||||
|
private var signerRequestModels: [SignerRequestModel]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
EmptyView()
|
if let signerRequestModel = signerRequestModels.first {
|
||||||
|
Section {
|
||||||
|
VStack {
|
||||||
|
Text(signerRequestModel.type.rawValue)
|
||||||
|
Text(signerRequestModel.returnType.rawValue)
|
||||||
|
Text(signerRequestModel.compressionType.rawValue)
|
||||||
|
Text(signerRequestModel.createdAt.description)
|
||||||
|
Text(signerRequestModel.callbackURL ?? "Unknown callbackURL")
|
||||||
|
Text(signerRequestModel.pubkey ?? "Unknown pubkey")
|
||||||
|
Text(signerRequestModel.sourceApplication ?? "Unknown sourceApplication")
|
||||||
|
Text(signerRequestModel.targetApplication ?? "Unknown targetApplication")
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button(
|
||||||
|
String(localized: "Allow", comment: "Button to allow a permission request."),
|
||||||
|
systemImage: "checkmark"
|
||||||
|
) {
|
||||||
|
signerRequestModel.approved = true
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
String(localized: "Deny", comment: "Button to deny a permission request."),
|
||||||
|
systemImage: "xmark"
|
||||||
|
) {
|
||||||
|
signerRequestModel.approved = false
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack {
|
||||||
|
Text(
|
||||||
|
"Nothing to approve yet",
|
||||||
|
comment:
|
||||||
|
"""
|
||||||
|
Text on Home tab to indicate that there are no permission requests to review for approval.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Why not explore your favorite Nostr app a bit?",
|
||||||
|
comment:
|
||||||
|
"""
|
||||||
|
Text on Home tab to suggest what the user could do instead
|
||||||
|
since there are no permission requests to review for approval.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
// Created by Terry Yiu on 1/20/25.
|
// Created by Terry Yiu on 1/20/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import NostrSDK
|
||||||
|
import SwiftData
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LoggedInView: View {
|
struct LoggedInView: View {
|
||||||
|
let publicKey: String
|
||||||
|
|
||||||
@State var selectedTab: YetiTab = .home
|
@State var selectedTab: YetiTab = .home
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -18,7 +22,7 @@ struct LoggedInView: View {
|
|||||||
case .home:
|
case .home:
|
||||||
HomeView()
|
HomeView()
|
||||||
case .permissions:
|
case .permissions:
|
||||||
PermissionsView()
|
PermissionsView(publicKey: publicKey)
|
||||||
case .history:
|
case .history:
|
||||||
HistoryView()
|
HistoryView()
|
||||||
case .settings:
|
case .settings:
|
||||||
@@ -64,5 +68,5 @@ enum YetiTab: CustomStringConvertible, CaseIterable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
LoggedInView()
|
LoggedInView(publicKey: Keypair()!.publicKey.hex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,82 +11,82 @@ import SwiftUI
|
|||||||
struct NostrClientView: View {
|
struct NostrClientView: View {
|
||||||
@Query private var nostrClientModels: [NostrClientModel]
|
@Query private var nostrClientModels: [NostrClientModel]
|
||||||
|
|
||||||
init(nostrClientModelId: String) {
|
init(nostrClientModelName: String) {
|
||||||
self._nostrClientModels = Query(filter: #Predicate<NostrClientModel> {
|
self._nostrClientModels = Query(filter: NostrClientModel.predicateByName(nostrClientModelName))
|
||||||
$0.id == nostrClientModelId
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var nostrClientModel: NostrClientModel {
|
var nostrClientModel: NostrClientModel? {
|
||||||
nostrClientModels.first!
|
nostrClientModels.first
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
let bindableNostrClientModel = Bindable(nostrClientModel)
|
if let nostrClientModel {
|
||||||
|
let bindableNostrClientModel = Bindable(nostrClientModel)
|
||||||
|
|
||||||
Text(bindableNostrClientModel.id)
|
Text(bindableNostrClientModel.name.wrappedValue)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
List {
|
List {
|
||||||
Toggle(
|
|
||||||
String(
|
|
||||||
localized: "Read your profile",
|
|
||||||
comment: "Permission toggle to allow Nostr client to read your profile."
|
|
||||||
),
|
|
||||||
isOn: bindableNostrClientModel.readPublicKeyPermission
|
|
||||||
)
|
|
||||||
Toggle(
|
|
||||||
String(
|
|
||||||
localized: "Get relays",
|
|
||||||
comment: "Permission toggle to allow Nostr client to get relays."
|
|
||||||
),
|
|
||||||
isOn: bindableNostrClientModel.getRelaysPermission
|
|
||||||
)
|
|
||||||
Toggle(
|
|
||||||
String(
|
|
||||||
localized: "Encrypt DMs",
|
|
||||||
comment: "Permission toggle to allow Nostr client to encrypt direct messages."
|
|
||||||
),
|
|
||||||
isOn: bindableNostrClientModel.nip44EncryptPermission
|
|
||||||
)
|
|
||||||
Toggle(
|
|
||||||
String(
|
|
||||||
localized: "Decrypt DMs",
|
|
||||||
comment: "Permission toggle to allow Nostr client to decrypt direct messages."
|
|
||||||
),
|
|
||||||
isOn: bindableNostrClientModel.nip44DecryptPermission
|
|
||||||
)
|
|
||||||
Toggle(
|
|
||||||
String(
|
|
||||||
localized: "Encrypt legacy DMs",
|
|
||||||
comment: "Permission toggle to allow Nostr client to encrypt legacy direct messages."
|
|
||||||
),
|
|
||||||
isOn: bindableNostrClientModel.nip04EncryptPermission
|
|
||||||
)
|
|
||||||
Toggle(
|
|
||||||
String(
|
|
||||||
localized: "Decrypt legacy DMs",
|
|
||||||
comment: "Permission toggle to allow Nostr client to decrypt legacy direct messages."
|
|
||||||
),
|
|
||||||
isOn: bindableNostrClientModel.nip04DecryptPermission
|
|
||||||
)
|
|
||||||
Toggle(
|
|
||||||
String(
|
|
||||||
localized: "Decrypt zap events",
|
|
||||||
comment: "Permission toggle to allow Nostr client to decrypt zap events."
|
|
||||||
),
|
|
||||||
isOn: bindableNostrClientModel.decryptZapEventPermission
|
|
||||||
)
|
|
||||||
|
|
||||||
ForEach(bindableNostrClientModel.signEventPermissions, id: \.self) { signEventPermissionModel in
|
|
||||||
Toggle(
|
Toggle(
|
||||||
String(
|
String(
|
||||||
localized: "Sign kind \(signEventPermissionModel.kind.wrappedValue.description) events",
|
localized: "Read your profile",
|
||||||
comment: "Permission toggle to allow Nostr client to sign events of a specific kind.."
|
comment: "Permission toggle to allow Nostr client to read your profile."
|
||||||
),
|
),
|
||||||
isOn: signEventPermissionModel.allowed
|
isOn: bindableNostrClientModel.readPublicKeyPermission
|
||||||
)
|
)
|
||||||
|
Toggle(
|
||||||
|
String(
|
||||||
|
localized: "Get relays",
|
||||||
|
comment: "Permission toggle to allow Nostr client to get relays."
|
||||||
|
),
|
||||||
|
isOn: bindableNostrClientModel.getRelaysPermission
|
||||||
|
)
|
||||||
|
Toggle(
|
||||||
|
String(
|
||||||
|
localized: "Encrypt DMs",
|
||||||
|
comment: "Permission toggle to allow Nostr client to encrypt direct messages."
|
||||||
|
),
|
||||||
|
isOn: bindableNostrClientModel.nip44EncryptPermission
|
||||||
|
)
|
||||||
|
Toggle(
|
||||||
|
String(
|
||||||
|
localized: "Decrypt DMs",
|
||||||
|
comment: "Permission toggle to allow Nostr client to decrypt direct messages."
|
||||||
|
),
|
||||||
|
isOn: bindableNostrClientModel.nip44DecryptPermission
|
||||||
|
)
|
||||||
|
Toggle(
|
||||||
|
String(
|
||||||
|
localized: "Encrypt legacy DMs",
|
||||||
|
comment: "Permission toggle to allow Nostr client to encrypt legacy direct messages."
|
||||||
|
),
|
||||||
|
isOn: bindableNostrClientModel.nip04EncryptPermission
|
||||||
|
)
|
||||||
|
Toggle(
|
||||||
|
String(
|
||||||
|
localized: "Decrypt legacy DMs",
|
||||||
|
comment: "Permission toggle to allow Nostr client to decrypt legacy direct messages."
|
||||||
|
),
|
||||||
|
isOn: bindableNostrClientModel.nip04DecryptPermission
|
||||||
|
)
|
||||||
|
Toggle(
|
||||||
|
String(
|
||||||
|
localized: "Decrypt zap events",
|
||||||
|
comment: "Permission toggle to allow Nostr client to decrypt zap events."
|
||||||
|
),
|
||||||
|
isOn: bindableNostrClientModel.decryptZapEventPermission
|
||||||
|
)
|
||||||
|
|
||||||
|
ForEach(bindableNostrClientModel.signEventPermissions, id: \.self) { signEventPermissionModel in
|
||||||
|
Toggle(
|
||||||
|
String(
|
||||||
|
localized: "Sign kind \(signEventPermissionModel.kind.wrappedValue.description) events",
|
||||||
|
comment: "Permission toggle to allow Nostr client to sign events of a specific kind.."
|
||||||
|
),
|
||||||
|
isOn: signEventPermissionModel.approved
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,13 +94,13 @@ struct NostrClientView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension NostrClientModel {
|
extension NostrClientModel {
|
||||||
static func predicateById(_ id: String) -> Predicate<NostrClientModel> {
|
static func predicateByName(_ name: String) -> Predicate<NostrClientModel> {
|
||||||
#Predicate<NostrClientModel> { nostrClientModel in
|
#Predicate<NostrClientModel> { nostrClientModel in
|
||||||
nostrClientModel.id == id
|
nostrClientModel.name == name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NostrClientView(nostrClientModelId: UUID().uuidString)
|
NostrClientView(nostrClientModelName: UUID().uuidString)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,41 +5,46 @@
|
|||||||
// Created by Terry Yiu on 1/20/25.
|
// Created by Terry Yiu on 1/20/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import NostrSDK
|
||||||
import SwiftData
|
import SwiftData
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PermissionsView: View {
|
struct PermissionsView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Query var profileSettingsModels: [ProfileSettingsModel]
|
||||||
@Query(sort: \NostrClientModel.id) var nostrClientModels: [NostrClientModel]
|
|
||||||
|
init(publicKey: String) {
|
||||||
|
var descriptor = FetchDescriptor<ProfileSettingsModel>(
|
||||||
|
predicate: #Predicate { profileSettingsModel in
|
||||||
|
profileSettingsModel.publicKey == publicKey
|
||||||
|
}
|
||||||
|
)
|
||||||
|
descriptor.fetchLimit = 1
|
||||||
|
|
||||||
|
self._profileSettingsModels = Query(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileSettingsModel: ProfileSettingsModel? {
|
||||||
|
profileSettingsModels.first
|
||||||
|
}
|
||||||
|
|
||||||
|
var nostrClientModels: [NostrClientModel] {
|
||||||
|
profileSettingsModel?.nostrClientModels ?? []
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
ForEach(nostrClientModels, id: \.self) { nostrClientModel in
|
ForEach(nostrClientModels, id: \.self) { nostrClientModel in
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
nostrClientModel.id,
|
nostrClientModel.name,
|
||||||
destination: NostrClientView(nostrClientModelId: nostrClientModel.id)
|
destination: NostrClientView(nostrClientModelName: nostrClientModel.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO Remove this dummy code
|
|
||||||
.onAppear(perform: addNostrClientModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Remove this dummy code
|
|
||||||
private func addNostrClientModel() {
|
|
||||||
withAnimation {
|
|
||||||
let newNostrClientModel = NostrClientModel(id: Date().timeIntervalSince1970.rounded().description)
|
|
||||||
newNostrClientModel.signEventPermissions = [
|
|
||||||
SignEventPermissionModel(kind: 1, allowed: true),
|
|
||||||
SignEventPermissionModel(kind: 31923, allowed: true)
|
|
||||||
]
|
|
||||||
modelContext.insert(newNostrClientModel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
PermissionsView()
|
PermissionsView(publicKey: Keypair()!.publicKey.hex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ You’re set for now. You’ll need to come back here with every new app and app
|
|||||||
profileSettingsModel.signingPolicy = signingPolicy
|
profileSettingsModel.signingPolicy = signingPolicy
|
||||||
modelContext.insert(profileSettingsModel)
|
modelContext.insert(profileSettingsModel)
|
||||||
|
|
||||||
generalSettingsModels.first!.activePublicKey = keypair.publicKey.hex
|
generalSettingsModels.first?.activePublicKey = keypair.publicKey.hex
|
||||||
})
|
})
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ struct YetiApp: App {
|
|||||||
init() {
|
init() {
|
||||||
let schema = Schema([
|
let schema = Schema([
|
||||||
GeneralSettingsModel.self,
|
GeneralSettingsModel.self,
|
||||||
ProfileSettingsModel.self
|
ProfileSettingsModel.self,
|
||||||
|
SignerRequestModel.self
|
||||||
])
|
])
|
||||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user