1
0
mirror of git://jb55.com/damus synced 2024-09-30 00:40:45 +00:00

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>
This commit is contained in:
Daniel D’Aquino 2024-01-30 07:41:49 +00:00 committed by William Casarin
parent a6b430284f
commit e649c49981
4 changed files with 274 additions and 17 deletions

View File

@ -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 */,

View File

@ -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)")
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)
}
}
return nil
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
}

View File

@ -0,0 +1,158 @@
//
// DamusPurpleAccountView.swift
// damus
//
// Created by Daniel DAquino 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
)
)
}

View File

@ -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()
}
}
}