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 }