Skip to content

Commit

Permalink
purple: add support for LN checkout flow
Browse files Browse the repository at this point in the history
This commit adds support for the LN checkout flow, and the Purple
landing page:

1. It adds a "learn more" button on the Damus Purple view, where the
   user can learn more
2. It adds new `damus:purple` urls to enable the LN checkout flow

Signed-off-by: Daniel D’Aquino <[email protected]>
Reviewed-by: William Casarin <[email protected]>
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
danieldaquino authored and jb55 committed Jan 18, 2024
1 parent 75a9b4d commit 534969e
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 35 deletions.
24 changes: 18 additions & 6 deletions damus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,6 @@
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */; };
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */; };
D74AAFC52B1538DF006CF0F4 /* NotificationExtensionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */; };
Expand All @@ -463,6 +460,9 @@
D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; };
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */; };
D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
Expand Down Expand Up @@ -491,6 +491,9 @@
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 */; };
D7ADD3DE2B53854300F104C4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
D7ADD3E02B538D4200F104C4 /* DamusPurpleURLSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */; };
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */; };
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
D7CB5D3B2B112FBB00AD4105 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; };
D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; };
Expand Down Expand Up @@ -1333,15 +1336,15 @@
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = "<group>"; };
D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExtensionState.swift; sourceTree = "<group>"; };
D74AAFCB2B155D07006CF0F4 /* MakeZapRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeZapRequest.swift; sourceTree = "<group>"; };
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapDataModel.swift; sourceTree = "<group>"; };
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; };
Expand All @@ -1355,6 +1358,9 @@
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>"; };
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURL.swift; sourceTree = "<group>"; };
D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURLSheetView.swift; sourceTree = "<group>"; };
D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleVerifyNpubView.swift; sourceTree = "<group>"; };
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = "<group>"; };
D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = "<group>"; };
D7CB5D442B116FE800AD4105 /* Contacts+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contacts+.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2562,6 +2568,8 @@
children = (
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */,
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */,
D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */,
D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */,
);
path = Purple;
sourceTree = "<group>";
Expand Down Expand Up @@ -2622,6 +2630,7 @@
children = (
D74F43092B23F0BE00425B75 /* DamusPurple.swift */,
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */,
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */,
);
path = Purple;
sourceTree = "<group>";
Expand Down Expand Up @@ -2912,6 +2921,7 @@
4CEF958D2A9CE650000F901B /* verifier.c in Sources */,
4C32B9342A9AD01A00DC3548 /* NdbProfile.swift in Sources */,
4C32B9332A99845B00DC3548 /* Ndb.swift in Sources */,
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */,
4C4793082A993E8900489948 /* refmap.c in Sources */,
4C4793072A993E6200489948 /* emitter.c in Sources */,
4C4793062A993E5300489948 /* json_parser.c in Sources */,
Expand Down Expand Up @@ -3045,6 +3055,7 @@
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
D7ADD3E02B538D4200F104C4 /* DamusPurpleURLSheetView.swift in Sources */,
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
4CA927632A290EB10098A105 /* EventTop.swift in Sources */,
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
Expand Down Expand Up @@ -3327,6 +3338,7 @@
D7CB5D4E2B11728000AD4105 /* NewEventsBits.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */,
D7ADD3DE2B53854300F104C4 /* DamusPurpleURL.swift in Sources */,
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
Expand Down
11 changes: 11 additions & 0 deletions damus/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum Sheets: Identifiable {
case filter
case user_status
case onboardingSuggestions
case purple(DamusPurpleURL)

static func zap(target: ZapTarget, lnurl: String) -> Sheets {
return .zap(ZapSheet(target: target, lnurl: lnurl))
Expand All @@ -48,6 +49,7 @@ enum Sheets: Identifiable {
case .select_wallet: return "select-wallet"
case .filter: return "filter"
case .onboardingSuggestions: return "onboarding-suggestions"
case .purple(let purple_url): return "purple" + purple_url.url_string()
}
}
}
Expand Down Expand Up @@ -330,6 +332,8 @@ struct ContentView: View {
.presentationDragIndicator(.visible)
case .onboardingSuggestions:
OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!))
case .purple(let purple_url):
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
}
}
.onOpenURL { url in
Expand All @@ -344,6 +348,7 @@ struct ContentView: View {
case .event(let ev): self.open_event(ev: ev)
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
case .script(let data): self.open_script(data)
case .purple(let purple_url): self.active_sheet = .purple(purple_url)
}
}
}
Expand Down Expand Up @@ -1043,9 +1048,15 @@ enum OpenResult {
case event(NostrEvent)
case wallet_connect(WalletConnectURL)
case script([UInt8])
case purple(DamusPurpleURL)
}

func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
if let purple_url = DamusPurpleURL.from_url(url: url) {
result(.purple(purple_url))
return
}

if let nwc = WalletConnectURL(str: url.absoluteString) {
result(.wallet_connect(nwc))
return
Expand Down
25 changes: 25 additions & 0 deletions damus/Models/Purple/DamusPurple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,30 @@ class DamusPurple: StoreObserverDelegate {
throw PurpleError.translation_no_response
}
}

func verify_npub_for_checkout(checkout_id: String) async throws {
var url = environment.get_base_url()
url.append(path: "/ln-checkout/\(checkout_id)/verify")

let (data, response) = try await make_nip98_authenticated_request(
method: .put,
url: url,
payload: nil,
payload_type: nil,
auth_keypair: self.keypair
)

if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Verified npub for checkout id `%s` with Damus Purple server", for: .damus_purple, checkout_id)
default:
Log.error("Error in verifying npub with Damus Purple. HTTP status code: %d; Response: %s; Checkout id: ", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown", checkout_id)
throw PurpleError.checkout_npub_verification_error
}
}

}
}

// MARK: API types
Expand Down Expand Up @@ -189,6 +213,7 @@ extension DamusPurple {
enum PurpleError: Error {
case translation_error(status_code: Int, response: Data)
case translation_no_response
case checkout_npub_verification_error
}

struct TranslationResult: Codable {
Expand Down
43 changes: 43 additions & 0 deletions damus/Models/Purple/DamusPurpleURL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// DamusPurpleURL.swift
// damus
//
// Created by Daniel Nogueira on 2024-01-13.
//

import Foundation

enum DamusPurpleURL {
case verify_npub(checkout_id: String)
case welcome(checkout_id: String)
case landing

static func from_url(url: URL) -> DamusPurpleURL? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil }
guard components.scheme == "damus" else { return nil }
switch components.path {
case "purple:verify":
guard let checkout_id: String = components.queryItems?.first(where: { $0.name == "id" })?.value else { return nil }
return .verify_npub(checkout_id: checkout_id)
case "purple:welcome":
guard let checkout_id: String = components.queryItems?.first(where: { $0.name == "id" })?.value else { return nil }
return .welcome(checkout_id: checkout_id)
case "purple:landing":
return .landing
default:
return nil
}
}

func url_string() -> String {
switch self {
case .verify_npub(let id):
return "damus:purple:verify?id=\(id)"
case .welcome(let id):
return "damus:purple:welcome?id=\(id)"
case .landing:
return "damus:purple:landing"
}
}

}
2 changes: 2 additions & 0 deletions damus/Util/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ class Constants {
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
static let PURPLE_API_PRODUCTION_BASE_URL: URL = URL(string: "https://purple.damus.io")!
static let PURPLE_API_TEST_BASE_URL: URL = URL(string: "http://127.0.0.1:8989")!
static let PURPLE_LANDING_PAGE_TEST_URL: URL = URL(string: "http://localhost:3000/purple")!
static let PURPLE_LANDING_PAGE_PRODUCTION_URL: URL = URL(string: "https://damus.io/purple")!
}
34 changes: 34 additions & 0 deletions damus/Views/Purple/DamusPurpleURLSheetView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// DamusPurpleURLSheetView.swift
// damus
//
// Created by Daniel D’Aquino on 2024-01-13.
//

import Foundation

import SwiftUI

struct DamusPurpleURLSheetView: View {
@Environment(\.dismiss) var dismiss
let damus_state: DamusState
let purple_url: DamusPurpleURL

var body: some View {
switch self.purple_url {
case .verify_npub(let checkout_id):
DamusPurpleVerifyNpubView(damus_state: damus_state, checkout_id: checkout_id)
case .welcome(_):
DamusPurpleWelcomeView()
case .landing:
DamusPurpleView(damus_state: damus_state)
}
}
}

struct DamusPurpleURLSheetView_Previews: PreviewProvider {
static var previews: some View {
DamusPurpleURLSheetView(damus_state: test_damus_state, purple_url: .verify_npub(checkout_id: "123"))
}
}

64 changes: 64 additions & 0 deletions damus/Views/Purple/DamusPurpleVerifyNpubView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// DamusPurpleVerifyNpubView.swift
// damus
//
// Created by Daniel D’Aquino on 2024-01-13.
//

import SwiftUI

struct DamusPurpleVerifyNpubView: View {
let damus_state: DamusState
let checkout_id: String
@State var verified: Bool = false

var body: some View {
ZStack {
Rectangle()
.background(Color.black)

VStack {
DamusPurpleLogoView()

VStack(alignment: .center, spacing: 30) {
Subtitle(NSLocalizedString("To continue your Purple subscription checkout, please verify your npub by clicking on the button below", comment: "Instruction on how to verify npub during Damus Purple checkout"))
.multilineTextAlignment(.center)

if !verified {
Button(action: {
Task {
try await damus_state.purple.verify_npub_for_checkout(checkout_id: checkout_id)
verified = true
}
}, label: {
HStack {
Spacer()
Text(NSLocalizedString("Verify my npub", comment: "Button label to verify the user's npub for the purpose of Purple subscription checkout"))
Spacer()
}
})
.padding(.horizontal, 30)
.buttonStyle(GradientButtonStyle())
}
else {
Text(NSLocalizedString("Verified! Please head back to the checkout page to continue", comment: "Instructions after the user has verified their npub for Damus Purple purchase checkout"))
.multilineTextAlignment(.center)
.foregroundColor(.green)
}

}
.padding([.trailing, .leading], 30)
.padding(.bottom, 20)
}
}
}

func Subtitle(_ txt: String) -> some View {
Text(txt)
.foregroundColor(.white.opacity(0.65))
}
}

#Preview {
DamusPurpleVerifyNpubView(damus_state: test_damus_state, checkout_id: "123")
}
Loading

0 comments on commit 534969e

Please sign in to comment.