damus

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

DamusPurpleView.swift (7899B)


      1 //
      2 //  DamusPurpleView.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2023-03-21.
      6 //
      7 
      8 import SwiftUI
      9 import StoreKit
     10 
     11 // MARK: - Helper structures
     12 
     13 enum AccountInfoState {
     14     case loading
     15     case loaded(account: DamusPurple.Account)
     16     case no_account
     17     case error(message: String)
     18 }
     19 
     20 // MARK: - Main view
     21 
     22 struct DamusPurpleView: View, DamusPurpleStoreKitManagerDelegate {
     23     let damus_state: DamusState
     24     let keypair: Keypair
     25     
     26     @State var my_account_info_state: AccountInfoState = .loading
     27     @State var products: ProductState
     28     @State var purchased: PurchasedProduct? = nil
     29     @State var selection: DamusPurple.StoreKitManager.DamusPurpleType = .yearly
     30     @State var show_welcome_sheet: Bool = false
     31     @State var account_uuid: UUID? = nil
     32     @State var iap_error: String? = nil     // TODO: Display this error to the user in some way.
     33     @State private var shouldDismissView = false
     34     
     35     @Environment(\.dismiss) var dismiss
     36     
     37     init(damus_state: DamusState) {
     38         self._products = State(wrappedValue: .loading)
     39         self.damus_state = damus_state
     40         self.keypair = damus_state.keypair
     41     }
     42     
     43     // MARK: - Top level view
     44     
     45     var body: some View {
     46         NavigationView {
     47             PurpleBackdrop {
     48                 ScrollView {
     49                     MainContent
     50                         .padding(.top, 75)
     51                 }
     52             }
     53             .navigationBarHidden(true)
     54             .navigationBarTitleDisplayMode(.inline)
     55             .navigationBarBackButtonHidden(true)
     56             .navigationBarItems(leading: BackNav())
     57         }
     58         .onReceive(handle_notify(.switched_timeline)) { _ in
     59             dismiss()
     60         }
     61         .onAppear {
     62             notify(.display_tabbar(false))
     63             Task {
     64                 await self.load_account()
     65                 // Assign this view as the delegate for the storekit manager to receive purchase updates
     66                 damus_state.purple.storekit_manager.delegate = self
     67                 // Fetch the account UUID to use for IAP purchases and to check if an IAP purchase is associated with the account
     68                 self.account_uuid = try await damus_state.purple.get_maybe_cached_uuid_for_account()
     69             }
     70         }
     71         .onDisappear {
     72             notify(.display_tabbar(true))
     73         }
     74         .onReceive(handle_notify(.purple_account_update), perform: { account in
     75             self.my_account_info_state = .loaded(account: account)
     76         })
     77         .task {
     78             await load_products()
     79         }
     80         .ignoresSafeArea(.all)
     81         .sheet(isPresented: $show_welcome_sheet, onDismiss: {
     82             shouldDismissView = true
     83         }, content: {
     84             DamusPurpleNewUserOnboardingView(damus_state: damus_state)
     85         })
     86     }
     87     
     88     // MARK: - Complex subviews
     89     
     90     var MainContent: some View {
     91         VStack {
     92             DamusPurpleView.LogoView()
     93             
     94             switch my_account_info_state {
     95                 case .loading:
     96                     ProgressView()
     97                         .progressViewStyle(.circular)
     98                 case .loaded(let account):
     99                     Group {
    100                         DamusPurpleAccountView(damus_state: damus_state, account: account)
    101                         
    102                         ProductStateView(account: account)
    103                     }
    104                 case .no_account:
    105                     MarketingContent
    106                 case .error(let message):
    107                     Text(message)
    108                         .foregroundStyle(.red)
    109                         .multilineTextAlignment(.center)
    110                         .padding()
    111             }
    112             
    113             Spacer()
    114         }
    115     }
    116     
    117     var MarketingContent: some View {
    118         VStack {
    119             DamusPurpleView.MarketingContentView(purple: damus_state.purple)
    120             
    121             VStack(alignment: .center) {
    122                 ProductStateView(account: nil)
    123             }
    124             .padding([.top], 20)
    125         }
    126     }
    127     
    128     func ProductStateView(account: DamusPurple.Account?) -> some View {
    129         Group {
    130             if damus_state.purple.enable_purple_iap_support {
    131                 if account?.active == true && purchased == nil {
    132                     // Account active + no IAP purchases = Bought through Lightning.
    133                     // Instruct the user to manage billing on the website
    134                     ManageOnWebsiteNote
    135                 }
    136                 else {
    137                     // If account is no longer active or was purchased via IAP, then show IAP purchase/manage options
    138                     if let account_uuid {
    139                         DamusPurpleView.IAPProductStateView(products: products, purchased: purchased, account_uuid: account_uuid, subscribe: subscribe)
    140                         if let iap_error {
    141                             Text("There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: \(iap_error)", comment: "In-app purchase error message for the user")
    142                                 .foregroundStyle(.red)
    143                                 .multilineTextAlignment(.center)
    144                                 .padding(.horizontal)
    145                         }
    146                     }
    147                     else {
    148                         ProgressView()
    149                             .progressViewStyle(.circular)
    150                     }
    151                 }
    152                 
    153             }
    154             else {
    155                 ManageOnWebsiteNote
    156             }
    157         }
    158     }
    159     
    160     var ManageOnWebsiteNote: some View {
    161         Text("Visit the Damus website on a web browser to manage billing", comment: "Instruction on how to manage billing externally")
    162             .font(.caption)
    163             .foregroundColor(.white.opacity(0.6))
    164             .multilineTextAlignment(.center)
    165     }
    166     
    167     // MARK: - State management
    168     
    169     func load_account() async {
    170         do {
    171             if let account = try await damus_state.purple.fetch_account(pubkey: damus_state.keypair.pubkey) {
    172                 self.my_account_info_state = .loaded(account: account)
    173                 return
    174             }
    175             self.my_account_info_state = .no_account
    176             return
    177         }
    178         catch {
    179             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"))
    180         }
    181     }
    182     
    183     func load_products() async {
    184         do {
    185             let products = try await self.damus_state.purple.storekit_manager.get_products()
    186             self.products = .loaded(products)
    187 
    188             print("loaded products", products)
    189         } catch {
    190             self.products = .failed
    191             print("Failed to fetch products: \(error.localizedDescription)")
    192         }
    193     }
    194     
    195     // For DamusPurple.StoreKitManager.Delegate conformance. This gets called by the StoreKitManager when a new product was purchased
    196     func product_was_purchased(product: DamusPurple.StoreKitManager.PurchasedProduct) {
    197         self.purchased = product
    198     }
    199     
    200     func subscribe(_ product: Product) async throws {
    201         do {
    202             try await self.damus_state.purple.make_iap_purchase(product: product)
    203             show_welcome_sheet = true
    204         }
    205         catch(let error) {
    206             self.iap_error = error.localizedDescription
    207         }
    208     }
    209 }
    210 
    211 struct DamusPurpleView_Previews: PreviewProvider {
    212     static var previews: some View {
    213         /*
    214         DamusPurpleView(products: [
    215             DamusProduct(name: "Yearly", id: "purpleyearly", price: Decimal(69.99)),
    216             DamusProduct(name: "Monthly", id: "purple", price: Decimal(6.99)),
    217         ])
    218          */
    219         
    220         DamusPurpleView(damus_state: test_damus_state)
    221     }
    222 }