Skip to content

Commit

Permalink
Damus Purple: Add npub authentication for account management API calls
Browse files Browse the repository at this point in the history
Testing
---------

PASS

Device: iPhone 15 Pro simulator
iOS: 17.2
Damus: This commit
damus-api: 626fb9665d8d6c576dd635d5224869cd9b69d190
Server: Ubuntu 22.04 (VM)
Setup:
1. On the server, delete the `mdb` database files to start from scratch
2. In iOS, reinstall the app if necessary to make sure there are no in-app purchases
3. Enable subscriptions support via developer settings with localhost test mode and restart app
4. Start server with mock parameters (Run `npm run dev`)

Steps:
1. Open top bar and click on "Purple"
2. Purple screen should appear and show both benefits and the purchase options. PASS
3. Click on "monthly". An Apple screen to confirm purchase should appear. PASS
4. Welcome screen with animation should appear. PASS
5. Click continue and restart app (Due to known issue tracked at #1814)
6. Post something
7. Gold star should appear beside your name
8. Look at the server logs. There should be some requests to create the account (POST), to send the receipt (POST), and to get account status
9. Go to purple view. There should be some information about the subscription, as well as a "manage" button. PASS
10. Click on "manage" button. An iOS sheet should appear allow the user to unsubscribe or manage their subscription to Damus Purple.

Closes: #1809
Signed-off-by: Daniel D’Aquino <[email protected]>
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
danieldaquino authored and jb55 committed Dec 24, 2023
1 parent 4703ed8 commit 5ca5420
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 23 deletions.
4 changes: 4 additions & 0 deletions damus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@
D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; };
D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; };
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; };
D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; };
D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
Expand Down Expand Up @@ -1273,6 +1274,7 @@
D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = "<group>"; };
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = "<group>"; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; };
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1896,6 +1898,7 @@
4C2B7BF12A71B6540049DEE7 /* Id.swift */,
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */,
D798D22B2B086C7400234419 /* NostrEvent+.swift */,
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */,
);
path = Nostr;
sourceTree = "<group>";
Expand Down Expand Up @@ -3028,6 +3031,7 @@
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */,
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
Expand Down
72 changes: 49 additions & 23 deletions damus/Models/Purple/DamusPurple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ class DamusPurple: StoreObserverDelegate {
func account_exists(pubkey: Pubkey) async -> Bool? {
guard let account_data = await self.get_account_data(pubkey: pubkey) else { return nil }

if let json = try? JSONSerialization.jsonObject(with: account_data, options: []) as? [String: Any],
let id = json["id"] as? String {
return id == pubkey.hex()
if let account_info = try? JSONDecoder().decode(AccountInfo.self, from: account_data) {
return account_info.pubkey == pubkey.hex()
}

return false
Expand All @@ -63,19 +62,27 @@ class DamusPurple: StoreObserverDelegate {

func create_account(pubkey: Pubkey) async throws {
let url = environment.get_base_url().appendingPathComponent("accounts")
var request = URLRequest(url: url)
request.httpMethod = "POST"

let payload: [String: String] = [
"pubkey": pubkey.hex()
]
let encoded_payload = try JSONEncoder().encode(payload)

request.httpBody = try JSONEncoder().encode(payload)
do {
let (_, _) = try await URLSession.shared.data(for: request)
return
} catch {
print("Failed to fetch data: \(error)")
Log.info("Creating account with Damus Purple server", for: .damus_purple)

let (data, response) = try await make_nip98_authenticated_request(
method: .post,
url: url,
payload: encoded_payload,
auth_keypair: self.keypair
)

if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Created an account with Damus Purple server", for: .damus_purple)
default:
Log.error("Error in creating account with Damus Purple. HTTP status code: %d", for: .damus_purple, httpResponse.statusCode)
}
}

return
Expand All @@ -95,26 +102,45 @@ class DamusPurple: StoreObserverDelegate {

do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
print(receiptData)

let url = environment.get_base_url().appendingPathComponent("accounts/\(keypair.pubkey.hex())/app-store-receipt")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = receiptData

do {
let (_, _) = try await URLSession.shared.data(for: request)
print("Sent receipt")
} catch {
print("Failed to fetch data: \(error)")
Log.info("Sending in-app purchase receipt to Damus Purple server", for: .damus_purple)

let (data, response) = try await make_nip98_authenticated_request(
method: .post,
url: url,
payload: receiptData,
auth_keypair: self.keypair
)

if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Sent in-app purchase receipt to Damus Purple server successfully", for: .damus_purple)
default:
Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d", for: .damus_purple, httpResponse.statusCode)
}
}

}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
catch {
Log.error("Couldn't read receipt data with error: %s", for: .damus_purple, error.localizedDescription)
}
}
}
}

// MARK: API types

extension DamusPurple {
fileprivate struct AccountInfo: Codable {
let pubkey: String
let created_at: UInt64
let expiry: UInt64?
let active: Bool
}
}

// MARK: Helper structures

extension DamusPurple {
Expand Down
41 changes: 41 additions & 0 deletions damus/Nostr/NIP98AuthenticatedRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// NIP98AuthenticatedRequest.swift
// damus
//
// Created by Daniel D’Aquino on 2023-12-15.
//

import Foundation

enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}

func make_nip98_authenticated_request(method: HTTPMethod, url: URL, payload: Data, auth_keypair: Keypair) async throws -> (data: Data, response: URLResponse) {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.httpBody = payload

let payload_hash = sha256(payload)
let payload_hash_hex = payload_hash.map({ String(format: "%02hhx", $0) }).joined()

let auth_note = NdbNote(
content: "",
keypair: auth_keypair,
kind: 27235,
tags: [
["u", url.absoluteString],
["method", method.rawValue],
["payload", payload_hash_hex]
],
createdAt: UInt32(Date().timeIntervalSince1970)
)
let auth_note_json_data: Data = try JSONEncoder().encode(auth_note)
let auth_note_base64: String = base64_encode(auth_note_json_data.bytes)

request.setValue("Nostr " + auth_note_base64, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request)
}
1 change: 1 addition & 0 deletions damus/Util/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum LogCategory: String {
case render
case storage
case push_notifications
case damus_purple
}

/// Damus structured logger
Expand Down

0 comments on commit 5ca5420

Please sign in to comment.