Add some support for signer requests

This commit is contained in:
2025-01-25 10:19:07 -05:00
parent e495d10d92
commit aae25543e0
16 changed files with 372 additions and 153 deletions

View File

@@ -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;

View File

@@ -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
} }
} }

View File

@@ -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
}
}
}

View File

@@ -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
} }
} }

View 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
}

View File

@@ -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 {

View File

@@ -7,9 +7,15 @@
"Add your key" : { "Add your key" : {
"comment" : "Title of view to add the users private key." "comment" : "Title of view to add the users 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 users entered private key." "comment" : "Button to go to the next view that adds the users entered private key."
}, },
@@ -49,9 +58,15 @@
"Next" : { "Next" : {
"comment" : "Button to go to the next view that adds the users entered private key." "comment" : "Button to go to the next view that adds the users 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."
}, },

View File

@@ -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
} }

View File

@@ -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)
} }

View File

@@ -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")
}
}
}
}
} }
} }

View File

@@ -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)
}
}
} }
} }

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -49,7 +49,7 @@ Youre set for now. Youll 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)
} }

View File

@@ -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)