Skip to content

Commit 3332c4a

Browse files
vveerrggclaude
andcommitted
feat: add NostrKey-v2 native SwiftUI scaffold and update screenshots
New v2 architecture with native crypto, relay, NIP-46, and SwiftUI screens. Add Monokai color assets. Update QA screenshots and project config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 376c0e3 commit 3332c4a

46 files changed

Lines changed: 3880 additions & 113 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

APP-STORE-SUBMISSION.md

Lines changed: 139 additions & 74 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,74 @@
11
# CLAUDE.md — nostrkey.app.ios.src
22

33
## What This Is
4-
NostrKey iOS app — native wrapper that runs the full NostrKey browser extension UI on iOS with native platform integrations (QR scanner, clipboard, lock screen npub display).
4+
NostrKey iOS app — a native SwiftUI Nostr identity authenticator. QR scanner as primary interface, Secure Enclave key storage, NIP-46 remote signing, Apple Wallet integration, and deep link relay management.
5+
6+
## Architecture (v2 — March 2026 Rebuild)
7+
Complete rewrite from WebView-wrapper to native SwiftUI. No more bundled browser extension. The app is a standalone authenticator that holds keys and signs on behalf of any Nostr client.
8+
9+
### Source: `NostrKey-v2/`
10+
```
11+
NostrKey-v2/
12+
├── App/ # SwiftUI app entry, AppState, ContentView, DeepLinkHandler
13+
├── Scanner/ # AVFoundation QR camera view
14+
├── Identity/ # KeyManager (Secure Enclave), NostrProfile, ProfileManager
15+
├── Relay/ # RelayManager, RelayInfo, NIP-11 metadata
16+
├── NIP46/ # NIP-46 remote signing session management
17+
├── Crypto/ # Bech32, NostrCrypto (nsec/npub encoding), event hashing
18+
├── UI/ # IdentityCardView, RelayListView, SettingsView
19+
├── Info.plist # App config (camera + Face ID permissions, deep link schemes)
20+
└── NostrKey.entitlements # App Groups + Keychain sharing
21+
```
22+
23+
### Legacy Source: `NostrKey/`
24+
The original WebView-wrapper app (v1.x) is preserved in `NostrKey/` for reference. The v2 project.yml points to `NostrKey-v2/` sources but still uses `NostrKey/Assets.xcassets` for icons.
525

626
## Ecosystem Position
7-
Mobile surface for NostrKey — the key management layer. Same role as the browser extension but on iOS. Uses dual-WKWebView architecture with IOSBridge.swift bridging JS to native APIs.
27+
The authenticator for Nostr. Holds keys in Secure Enclave, signs via NIP-46 for any client. Shares keys with the Safari extension via App Group Keychain (`group.com.nostrkey`).
828

929
## Current Version
10-
v1.1.1 — Bundled extension v1.5.5 — App Store submission in progress
30+
v2.0.0 (Build 1) — Native SwiftUI rebuild
1131

1232
## Tech Stack
13-
- Swift 5.9, Xcode 15.0+
14-
- Dual-WKWebView (background + UI)
15-
- IOSBridge.swift (`WKScriptMessageHandler`)
16-
- ios-polyfill.js maps `chrome.storage`/`chrome.runtime` to bridge calls
17-
- UserDefaults for storage
18-
- AVFoundation for QR scanning
19-
- xcodegen for project generation
33+
- Swift 5.9, SwiftUI, Xcode 16.0+, iOS 17.0+
34+
- AVFoundation (QR camera)
35+
- Security framework + CryptoKit (Keychain with Secure Enclave protection)
36+
- LocalAuthentication (Face ID / Touch ID)
37+
- CoreImage (QR code generation)
38+
- URLSessionWebSocketTask (NIP-46 relay communication)
39+
- XcodeGen for project generation
2040

2141
## Build
2242
```bash
2343
xcodegen generate
2444
open NostrKey.xcodeproj
25-
# Build & Run in Xcode
45+
# Build & Run in Xcode (requires physical device for camera + Secure Enclave)
2646
```
2747

28-
## Key Differences from Browser Extension
29-
- Storage: UserDefaults (not `chrome.storage`)
30-
- QR scanning: AVFoundation (native)
31-
- No `window.nostr` injection
32-
- No cross-device sync yet
33-
- Lock screen QR code + npub display (iOS-only features)
48+
## Key Architecture Decisions
49+
- **SwiftUI-only** — no UIKit except for AVCaptureSession (camera requires UIKit)
50+
- **Secure Enclave via Keychain** — secp256k1 keys stored with biometric access policy
51+
- **App Group sharing**`group.com.nostrkey` shared Keychain + UserDefaults
52+
- **NIP-46 over WebSocket** — remote signing without key exposure
53+
- **Deep links**`nostrkey://add-relay`, `nostrkey://connect`, `nostrconnect://`
54+
- **No web views** — pure native UI, no embedded browser extension
55+
56+
## TODO: Before Shipping
57+
- [ ] Integrate swift-secp256k1 package for real key derivation + Schnorr signing
58+
- [ ] Complete NIP-46 message parsing and encrypted response flow
59+
- [ ] Apple Wallet pass generation (PassKit + server-side signing)
60+
- [ ] Pass update push notification service
61+
- [ ] NIP-49 (ncryptsec) encrypted relay backup
62+
- [ ] App Store screenshots for authenticator flow
3463

35-
## Planned
36-
- App Store listing
37-
- App Groups (share profiles between iOS app ↔ Safari extension)
38-
- Biometric unlock (Face ID / Touch ID)
39-
- Deep links (`nostr:` URIs)
64+
## Deep Link Schemes
65+
- `nostrkey://add-relay?url=wss://...&name=...&paid=true`
66+
- `nostrkey://connect?pubkey=...&relay=wss://...`
67+
- `nostrkey://import-keys?nsec=nsec1...`
68+
- `nostrkey://wallet-pass?npub=npub1...`
69+
- `nostrconnect://pubkey?relay=wss://...&secret=...`
4070

4171
## Related Repos
42-
- `nostrkey.browser.plugin.src`core extension code (bundled here as web assets)
72+
- `nostrkey.browser.plugin.src`Safari/Chrome extension (NIP-07)
4373
- `nostrkey.app.android.src` — Android equivalent
44-
- `nostrkey.bizdocs.src` — business strategy
74+
- `nostrkey.bizdocs.src` — business strategy (see NostrKey-App-Architecture.md)

NostrKey-v2/App/AppState.swift

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import SwiftUI
2+
import Combine
3+
4+
/// Central application state shared across all views
5+
@MainActor
6+
class AppState: ObservableObject {
7+
// MARK: - Published State
8+
9+
/// The active Nostr identity (nil if no keys generated yet)
10+
@Published var activeProfile: NostrProfile?
11+
12+
/// All stored profiles
13+
@Published var profiles: [NostrProfile] = []
14+
15+
/// Connected relays
16+
@Published var relays: [RelayInfo] = []
17+
18+
/// Current tab in the main interface
19+
@Published var selectedTab: AppTab = .home
20+
21+
/// Whether we're showing the onboarding flow
22+
@Published var showOnboarding: Bool = false
23+
24+
/// Active NIP-46 sessions
25+
@Published var activeSessions: [NIP46Session] = []
26+
27+
// MARK: - Managers
28+
29+
let keyManager = KeyManager()
30+
let relayManager = RelayManager()
31+
32+
// MARK: - Initialization
33+
34+
init() {
35+
loadProfiles()
36+
loadRelays()
37+
}
38+
39+
// MARK: - Profile Management
40+
41+
func loadProfiles() {
42+
profiles = keyManager.loadProfiles()
43+
activeProfile = profiles.first(where: { $0.isActive }) ?? profiles.first
44+
45+
if profiles.isEmpty {
46+
showOnboarding = true
47+
}
48+
}
49+
50+
func createNewIdentity(name: String = "Default") async throws {
51+
let profile = try keyManager.generateKeyPair(name: name)
52+
profiles.append(profile)
53+
activeProfile = profile
54+
showOnboarding = false
55+
saveProfiles()
56+
}
57+
58+
func importKeys(nsec: String, name: String = "Imported") throws {
59+
let profile = try keyManager.importFromNsec(nsec, name: name)
60+
profiles.append(profile)
61+
activeProfile = profile
62+
showOnboarding = false
63+
saveProfiles()
64+
}
65+
66+
func saveProfiles() {
67+
keyManager.saveProfiles(profiles)
68+
}
69+
70+
// MARK: - Relay Management
71+
72+
func loadRelays() {
73+
relays = relayManager.loadRelays()
74+
}
75+
76+
func addRelay(url: String, name: String? = nil, paid: Bool = false) {
77+
let relay = RelayInfo(
78+
url: url,
79+
name: name ?? url,
80+
paid: paid,
81+
addedAt: Date()
82+
)
83+
if !relays.contains(where: { $0.url == url }) {
84+
relays.append(relay)
85+
relayManager.saveRelays(relays)
86+
}
87+
}
88+
89+
func removeRelay(at offsets: IndexSet) {
90+
relays.remove(atOffsets: offsets)
91+
relayManager.saveRelays(relays)
92+
}
93+
}
94+
95+
// MARK: - App Tab
96+
97+
enum AppTab: String, CaseIterable {
98+
case home = "Home"
99+
case scanner = "Scanner"
100+
case identity = "Identity"
101+
case relays = "Relays"
102+
case settings = "Settings"
103+
104+
var icon: String {
105+
switch self {
106+
case .home: return "house.fill"
107+
case .scanner: return "qrcode.viewfinder"
108+
case .identity: return "person.crop.circle"
109+
case .relays: return "network"
110+
case .settings: return "gearshape"
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)