commit e649c49981a26c121b9a81c10677dbdbe77004c7
parent a6b430284f46a3492c045ce2372bc742ef1755c7
Author: Daniel D’Aquino <daniel@daquino.me>
Date: Tue, 30 Jan 2024 07:41:49 +0000
purple: adapt to integrate better with the LN flow
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
4 files changed, 275 insertions(+), 18 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -442,6 +442,7 @@
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
D723411A2B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; };
+ D724D8272B64B40B00ABE789 /* DamusPurpleAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D724D8262B64B40B00ABE789 /* DamusPurpleAccountView.swift */; };
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; };
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; };
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
@@ -1335,6 +1336,7 @@
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; };
D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleEnvironment.swift; sourceTree = "<group>"; };
D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; };
+ D724D8262B64B40B00ABE789 /* DamusPurpleAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleAccountView.swift; sourceTree = "<group>"; };
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = "<group>"; };
D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = "<group>"; };
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
@@ -2576,6 +2578,7 @@
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */,
D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */,
D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */,
+ D724D8262B64B40B00ABE789 /* DamusPurpleAccountView.swift */,
);
path = Purple;
sourceTree = "<group>";
@@ -3213,6 +3216,7 @@
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
50A16FFD2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift in Sources */,
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
+ D724D8272B64B40B00ABE789 /* DamusPurpleAccountView.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */,
4CDD1AE02A6B305F001CD4DF /* NdbTagElem.swift in Sources */,
diff --git a/damus/Models/Purple/DamusPurple.swift b/damus/Models/Purple/DamusPurple.swift
@@ -37,7 +37,7 @@ class DamusPurple: StoreObserverDelegate {
return cached_result
}
- guard let data = await self.get_account_data(pubkey: pubkey) else { return nil }
+ guard let data = try? await self.get_account_data(pubkey: pubkey) else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return nil }
@@ -52,7 +52,7 @@ class DamusPurple: StoreObserverDelegate {
}
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 = try? await self.get_account_data(pubkey: pubkey) else { return nil }
if let account_info = try? JSONDecoder().decode(AccountInfo.self, from: account_data) {
return account_info.pubkey == pubkey.hex()
@@ -61,19 +61,33 @@ class DamusPurple: StoreObserverDelegate {
return false
}
- func get_account_data(pubkey: Pubkey) async -> Data? {
+ func get_account(pubkey: Pubkey) async throws -> Account? {
+ guard let data = try await self.get_account_data(pubkey: pubkey) else { return nil }
+ return Account.from(json_data: data)
+ }
+
+ func get_account_data(pubkey: Pubkey) async throws -> Data? {
let url = environment.api_base_url().appendingPathComponent("accounts/\(pubkey.hex())")
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
-
- do {
- let (data, _) = try await URLSession.shared.data(for: request)
- return data
- } catch {
- print("Failed to fetch data: \(error)")
- }
- return nil
+ let (data, response) = try await make_nip98_authenticated_request(
+ method: .get,
+ url: url,
+ payload: nil,
+ payload_type: nil,
+ auth_keypair: self.keypair
+ )
+
+ if let httpResponse = response as? HTTPURLResponse {
+ switch httpResponse.statusCode {
+ case 200:
+ return data
+ case 404:
+ return nil
+ default:
+ throw PurpleError.http_response_error(status_code: httpResponse.statusCode, response: data)
+ }
+ }
+ throw PurpleError.error_processing_response
}
func create_account(pubkey: Pubkey) async throws {
@@ -207,6 +221,42 @@ class DamusPurple: StoreObserverDelegate {
formatter.numberStyle = .ordinal
return formatter.string(from: NSNumber(integerLiteral: number))
}
+
+ static func from(account: Account) -> Self {
+ return UserBadgeInfo(active: account.active, subscriber_number: Int(account.subscriber_number))
+ }
+ }
+
+ struct Account {
+ let pubkey: Pubkey
+ let created_at: Date
+ let expiry: Date
+ let subscriber_number: UInt
+ let active: Bool
+
+ static func from(json_data: Data) -> Self? {
+ guard let payload = try? JSONDecoder().decode(Payload.self, from: json_data) else { return nil }
+ return Self.from(payload: payload)
+ }
+
+ static func from(payload: Payload) -> Self? {
+ guard let pubkey = Pubkey(hex: payload.pubkey) else { return nil }
+ return Self(
+ pubkey: pubkey,
+ created_at: Date.init(timeIntervalSince1970: TimeInterval(payload.created_at)),
+ expiry: Date.init(timeIntervalSince1970: TimeInterval(payload.expiry)),
+ subscriber_number: payload.subscriber_number,
+ active: payload.active
+ )
+ }
+
+ struct Payload: Codable {
+ let pubkey: String // Hex-encoded string
+ let created_at: UInt64 // Unix timestamp
+ let expiry: UInt64 // Unix timestamp
+ let subscriber_number: UInt
+ let active: Bool
+ }
}
}
@@ -226,6 +276,8 @@ extension DamusPurple {
extension DamusPurple {
enum PurpleError: Error {
case translation_error(status_code: Int, response: Data)
+ case http_response_error(status_code: Int, response: Data)
+ case error_processing_response
case translation_no_response
case checkout_npub_verification_error
}
diff --git a/damus/Views/Purple/DamusPurpleAccountView.swift b/damus/Views/Purple/DamusPurpleAccountView.swift
@@ -0,0 +1,158 @@
+//
+// DamusPurpleAccountView.swift
+// damus
+//
+// Created by Daniel D’Aquino on 2024-01-26.
+//
+
+import SwiftUI
+
+struct DamusPurpleAccountView: View {
+ var colorScheme: ColorScheme = .dark
+ let damus_state: DamusState
+ let account: DamusPurple.Account
+ let pfp_size: CGFloat = 90.0
+
+ var body: some View {
+ VStack {
+ ProfilePicView(pubkey: account.pubkey, size: pfp_size, highlight: .custom(Color.black.opacity(0.4), 1.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
+ .background(Color.black.opacity(0.4).clipShape(Circle()))
+ .shadow(color: .black, radius: 10, x: 0.0, y: 5)
+
+ profile_name
+
+ if account.active {
+ active_account_badge
+ }
+ else {
+ inactive_account_badge
+ }
+
+ // TODO: Generalize this view instead of setting up dividers and paddings manually
+ VStack {
+ HStack {
+ Text(NSLocalizedString("Expiry date", comment: "Label for Purple subscription expiry date"))
+ Spacer()
+ Text(DateFormatter.localizedString(from: account.expiry, dateStyle: .short, timeStyle: .none))
+ }
+ .padding(.horizontal)
+ .padding(.top, 20)
+
+ Divider()
+ .padding(.horizontal)
+ .padding(.vertical, 10)
+
+ HStack {
+ Text(NSLocalizedString("Account creation", comment: "Label for Purple account creation date"))
+ Spacer()
+ Text(DateFormatter.localizedString(from: account.created_at, dateStyle: .short, timeStyle: .none))
+ }
+ .padding(.horizontal)
+
+ Divider()
+ .padding(.horizontal)
+ .padding(.vertical, 10)
+
+ HStack {
+ Text(NSLocalizedString("Subscriber number", comment: "Label for Purple account subscriber number"))
+ Spacer()
+ Text("#\(account.subscriber_number)")
+ }
+ .padding(.horizontal)
+ .padding(.bottom, 20)
+ }
+ .foregroundColor(.white.opacity(0.8))
+ .preferredColorScheme(.dark)
+ .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
+ .padding()
+
+ Text(NSLocalizedString("Visit the Damus website on a web browser to manage billing", comment: "Instruction on how to manage billing externally"))
+ .font(.caption)
+ .foregroundColor(.white.opacity(0.6))
+ .multilineTextAlignment(.center)
+ }
+ }
+
+ var profile_name: some View {
+ let display_name = self.profile_display_name()
+ return HStack(alignment: .center, spacing: 5) {
+ Text(display_name)
+ .font(.title)
+ .bold()
+ .foregroundStyle(.white)
+
+ SupporterBadge(
+ percent: nil,
+ purple_badge_info: DamusPurple.UserBadgeInfo.from(account: account),
+ style: .full
+ )
+ }
+ }
+
+ var active_account_badge: some View {
+ HStack(spacing: 3) {
+ Image("check-circle.fill")
+ .resizable()
+ .frame(width: 15, height: 15)
+
+ Text(NSLocalizedString("Active account", comment: "Badge indicating user has an active Damus Purple account"))
+ .font(.caption)
+ .bold()
+ }
+ .foregroundColor(Color.white)
+ .padding(.vertical, 3)
+ .padding(.horizontal, 8)
+ .background(PinkGradient)
+ .cornerRadius(30.0)
+ }
+
+ var inactive_account_badge: some View {
+ HStack(spacing: 3) {
+ Image("warning")
+ .resizable()
+ .frame(width: 15, height: 15)
+
+ Text(NSLocalizedString("Expired account", comment: "Badge indicating user has an expired Damus Purple account"))
+ .font(.caption)
+ .bold()
+ }
+ .foregroundColor(DamusColors.danger)
+ .padding(.vertical, 3)
+ .padding(.horizontal, 8)
+ .background(DamusColors.dangerTertiary)
+ .cornerRadius(30.0)
+ }
+
+ func profile_display_name() -> String {
+ let profile_txn: NdbTxn<ProfileRecord?>? = damus_state.profiles.lookup_with_timestamp(account.pubkey)
+ let profile: NdbProfile? = profile_txn?.unsafeUnownedValue?.profile
+ let display_name = parse_display_name(profile: profile, pubkey: account.pubkey).displayName
+ return display_name
+ }
+}
+
+#Preview("Active") {
+ DamusPurpleAccountView(
+ damus_state: test_damus_state,
+ account: DamusPurple.Account(
+ pubkey: test_pubkey,
+ created_at: Date.now,
+ expiry: Date.init(timeIntervalSinceNow: 60 * 60 * 24 * 30),
+ subscriber_number: 7,
+ active: true
+ )
+ )
+}
+
+#Preview("Expired") {
+ DamusPurpleAccountView(
+ damus_state: test_damus_state,
+ account: DamusPurple.Account(
+ pubkey: test_pubkey,
+ created_at: Date.init(timeIntervalSinceNow: -60 * 60 * 24 * 37),
+ expiry: Date.init(timeIntervalSinceNow: -60 * 60 * 24 * 7),
+ subscriber_number: 7,
+ active: false
+ )
+ )
+}
diff --git a/damus/Views/Purple/DamusPurpleView.swift b/damus/Views/Purple/DamusPurpleView.swift
@@ -27,6 +27,13 @@ enum ProductState {
}
}
+enum AccountInfoState {
+ case loading
+ case loaded(account: DamusPurple.Account)
+ case no_account
+ case error(message: String)
+}
+
func non_discounted_price(_ product: Product) -> String {
return (product.price * 1.1984569224).formatted(product.priceFormatStyle)
}
@@ -45,6 +52,7 @@ struct DamusPurpleView: View {
let damus_state: DamusState
let keypair: Keypair
+ @State var my_account_info_state: AccountInfoState = .loading
@State var products: ProductState
@State var purchased: PurchasedProduct? = nil
@State var selection: DamusPurpleType = .yearly
@@ -86,6 +94,9 @@ struct DamusPurpleView: View {
}
.onAppear {
notify(.display_tabbar(false))
+ Task {
+ await self.load_account()
+ }
}
.onDisappear {
notify(.display_tabbar(true))
@@ -123,6 +134,20 @@ struct DamusPurpleView: View {
.manageSubscriptionsSheet(isPresented: $show_manage_subscriptions)
}
+ func load_account() async {
+ do {
+ if let account = try await damus_state.purple.get_account(pubkey: damus_state.keypair.pubkey) {
+ self.my_account_info_state = .loaded(account: account)
+ return
+ }
+ self.my_account_info_state = .no_account
+ return
+ }
+ catch {
+ self.my_account_info_state = .error(message: NSLocalizedString("There was an error loading your account. Please try again later. If problem persists, please contact us at support@damus.io", comment: "Error label when Purple account information fails to load"))
+ }
+ }
+
func update_user_settings_to_purple() {
if damus_state.settings.translation_service == .none {
set_translation_settings_to_purple()
@@ -357,6 +382,27 @@ struct DamusPurpleView: View {
VStack {
DamusPurpleLogoView()
+ switch my_account_info_state {
+ case .loading:
+ ProgressView()
+ .progressViewStyle(.circular)
+ case .loaded(let account):
+ DamusPurpleAccountView(damus_state: damus_state, account: account)
+ case .no_account:
+ MarketingContent
+ case .error(let message):
+ Text(message)
+ .foregroundStyle(.red)
+ .multilineTextAlignment(.center)
+ .padding()
+ }
+
+ Spacer()
+ }
+ }
+
+ var MarketingContent: some View {
+ VStack {
VStack(alignment: .leading, spacing: 30) {
Subtitle(NSLocalizedString("Help us stay independent in our mission for Freedom tech with our Purple subscription, and look cool doing it!", comment: "Damus purple subscription pitch"))
.multilineTextAlignment(.center)
@@ -414,8 +460,8 @@ struct DamusPurpleView: View {
NSLocalizedString("Learn more", comment: "Label for a link to the Damus Purple landing page"),
destination: damus_state.purple.environment.purple_landing_page_url()
)
- .foregroundColor(DamusColors.pink)
- .padding()
+ .foregroundColor(DamusColors.pink)
+ .padding()
Spacer()
}
@@ -427,9 +473,6 @@ struct DamusPurpleView: View {
ProductStateView
}
.padding([.top], 20)
-
-
- Spacer()
}
}
}