damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Models/Purple/DamusPurple.swift | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Adamus/Views/Purple/DamusPurpleAccountView.swift | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Purple/DamusPurpleView.swift | 53++++++++++++++++++++++++++++++++++++++++++++++++-----
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() } } }