Damus Purple: Add npub authentication for account management API calls
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 damus-io#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: https://github.com/damus-io/damus/issues/1809 Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
committed by
William Casarin
parent
4703ed80a7
commit
5ca5420ce2
@@ -472,6 +472,7 @@
|
|||||||
D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
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 */; };
|
D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; };
|
||||||
D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; };
|
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 */; };
|
D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; };
|
||||||
D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.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"; }; };
|
D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
|
||||||
@@ -1273,6 +1274,7 @@
|
|||||||
D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
|
||||||
@@ -1896,6 +1898,7 @@
|
|||||||
4C2B7BF12A71B6540049DEE7 /* Id.swift */,
|
4C2B7BF12A71B6540049DEE7 /* Id.swift */,
|
||||||
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */,
|
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */,
|
||||||
D798D22B2B086C7400234419 /* NostrEvent+.swift */,
|
D798D22B2B086C7400234419 /* NostrEvent+.swift */,
|
||||||
|
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */,
|
||||||
);
|
);
|
||||||
path = Nostr;
|
path = Nostr;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3028,6 +3031,7 @@
|
|||||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||||
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
|
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
|
||||||
|
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */,
|
||||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
||||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||||
|
|||||||
@@ -38,9 +38,8 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
func account_exists(pubkey: Pubkey) async -> Bool? {
|
func account_exists(pubkey: Pubkey) async -> Bool? {
|
||||||
guard let account_data = await self.get_account_data(pubkey: pubkey) else { return nil }
|
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],
|
if let account_info = try? JSONDecoder().decode(AccountInfo.self, from: account_data) {
|
||||||
let id = json["id"] as? String {
|
return account_info.pubkey == pubkey.hex()
|
||||||
return id == pubkey.hex()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -63,19 +62,27 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
|
|
||||||
func create_account(pubkey: Pubkey) async throws {
|
func create_account(pubkey: Pubkey) async throws {
|
||||||
let url = environment.get_base_url().appendingPathComponent("accounts")
|
let url = environment.get_base_url().appendingPathComponent("accounts")
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
|
|
||||||
let payload: [String: String] = [
|
let payload: [String: String] = [
|
||||||
"pubkey": pubkey.hex()
|
"pubkey": pubkey.hex()
|
||||||
]
|
]
|
||||||
|
let encoded_payload = try JSONEncoder().encode(payload)
|
||||||
|
|
||||||
request.httpBody = try JSONEncoder().encode(payload)
|
Log.info("Creating account with Damus Purple server", for: .damus_purple)
|
||||||
do {
|
|
||||||
let (_, _) = try await URLSession.shared.data(for: request)
|
let (data, response) = try await make_nip98_authenticated_request(
|
||||||
return
|
method: .post,
|
||||||
} catch {
|
url: url,
|
||||||
print("Failed to fetch data: \(error)")
|
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
|
return
|
||||||
@@ -95,26 +102,45 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
|
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
|
||||||
print(receiptData)
|
|
||||||
|
|
||||||
let url = environment.get_base_url().appendingPathComponent("accounts/\(keypair.pubkey.hex())/app-store-receipt")
|
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 {
|
Log.info("Sending in-app purchase receipt to Damus Purple server", for: .damus_purple)
|
||||||
let (_, _) = try await URLSession.shared.data(for: request)
|
|
||||||
print("Sent receipt")
|
let (data, response) = try await make_nip98_authenticated_request(
|
||||||
} catch {
|
method: .post,
|
||||||
print("Failed to fetch data: \(error)")
|
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
|
// MARK: Helper structures
|
||||||
|
|
||||||
extension DamusPurple {
|
extension DamusPurple {
|
||||||
|
|||||||
41
damus/Nostr/NIP98AuthenticatedRequest.swift
Normal file
41
damus/Nostr/NIP98AuthenticatedRequest.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ enum LogCategory: String {
|
|||||||
case render
|
case render
|
||||||
case storage
|
case storage
|
||||||
case push_notifications
|
case push_notifications
|
||||||
|
case damus_purple
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Damus structured logger
|
/// Damus structured logger
|
||||||
|
|||||||
Reference in New Issue
Block a user