Merge branch 'iap-improvements'
Pull a few patches from v1.7-rc1 purple: show welcome sheet after ln payment iap: add loading spinner to purchase actions
This commit is contained in:
@@ -29,6 +29,7 @@ enum Sheets: Identifiable {
|
|||||||
case user_status
|
case user_status
|
||||||
case onboardingSuggestions
|
case onboardingSuggestions
|
||||||
case purple(DamusPurpleURL)
|
case purple(DamusPurpleURL)
|
||||||
|
case purple_onboarding
|
||||||
|
|
||||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||||
@@ -50,6 +51,7 @@ enum Sheets: Identifiable {
|
|||||||
case .filter: return "filter"
|
case .filter: return "filter"
|
||||||
case .onboardingSuggestions: return "onboarding-suggestions"
|
case .onboardingSuggestions: return "onboarding-suggestions"
|
||||||
case .purple(let purple_url): return "purple" + purple_url.url_string()
|
case .purple(let purple_url): return "purple" + purple_url.url_string()
|
||||||
|
case .purple_onboarding: return "purple_onboarding"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,6 +336,8 @@ struct ContentView: View {
|
|||||||
OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!))
|
OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!))
|
||||||
case .purple(let purple_url):
|
case .purple(let purple_url):
|
||||||
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
|
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
|
||||||
|
case .purple_onboarding:
|
||||||
|
DamusPurpleNewUserOnboardingView(damus_state: damus_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
@@ -343,12 +347,26 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch res {
|
switch res {
|
||||||
case .filter(let filt): self.open_search(filt: filt)
|
case .filter(let filt): self.open_search(filt: filt)
|
||||||
case .profile(let pk): self.open_profile(pubkey: pk)
|
case .profile(let pk): self.open_profile(pubkey: pk)
|
||||||
case .event(let ev): self.open_event(ev: ev)
|
case .event(let ev): self.open_event(ev: ev)
|
||||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
||||||
case .script(let data): self.open_script(data)
|
case .script(let data): self.open_script(data)
|
||||||
case .purple(let purple_url): self.active_sheet = .purple(purple_url)
|
case .purple(let purple_url):
|
||||||
|
if case let .welcome(checkout_id) = purple_url.variant {
|
||||||
|
// If this is a welcome link, do the following before showing the onboarding screen:
|
||||||
|
// 1. Check if this is legitimate and good to go.
|
||||||
|
// 2. Mark as complete if this is good to go.
|
||||||
|
Task {
|
||||||
|
let is_good_to_go = try? await damus_state.purple.check_and_mark_ln_checkout_is_good_to_go(checkout_id: checkout_id)
|
||||||
|
if is_good_to_go == true {
|
||||||
|
self.active_sheet = .purple(purple_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.active_sheet = .purple(purple_url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,6 +486,21 @@ struct ContentView: View {
|
|||||||
} else {
|
} else {
|
||||||
print("txn: NOSTRDB FAILED TO REOPEN closed:\(damus_state.ndb.is_closed)")
|
print("txn: NOSTRDB FAILED TO REOPEN closed:\(damus_state.ndb.is_closed)")
|
||||||
}
|
}
|
||||||
|
if damus_state.purple.checkout_ids_in_progress.count > 0 {
|
||||||
|
// For extra assurance, run this after one second, to avoid race conditions if the app is also handling a damus purple welcome url.
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
Task {
|
||||||
|
// TODO: Improve UX for renewals (#2013)
|
||||||
|
let freshly_completed_checkout_ids = try? await damus_state.purple.check_status_of_checkouts_in_progress()
|
||||||
|
let there_is_a_completed_checkout: Bool = (freshly_completed_checkout_ids?.count ?? 0) > 0
|
||||||
|
let account_info = try await damus_state.purple.fetch_account(pubkey: self.keypair.pubkey)
|
||||||
|
if there_is_a_completed_checkout == true && account_info?.active == true {
|
||||||
|
// Show welcome sheet
|
||||||
|
self.active_sheet = .purple_onboarding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||||
guard let damus_state else { return }
|
guard let damus_state else { return }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
let settings: UserSettingsStore
|
let settings: UserSettingsStore
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
var storekit_manager: StoreKitManager
|
var storekit_manager: StoreKitManager
|
||||||
|
var checkout_ids_in_progress: Set<String> = []
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var account_cache: [Pubkey: Account]
|
var account_cache: [Pubkey: Account]
|
||||||
@@ -243,6 +244,65 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func fetch_ln_checkout_object(checkout_id: String) async throws -> LNCheckoutInfo? {
|
||||||
|
let url = environment.api_base_url().appendingPathComponent("ln-checkout/\(checkout_id)")
|
||||||
|
|
||||||
|
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 try JSONDecoder().decode(LNCheckoutInfo.self, from: data)
|
||||||
|
case 404:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
throw PurpleError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw PurpleError.error_processing_response
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
/// This function checks the status of all checkout objects in progress with the server, and it does two things:
|
||||||
|
/// - It returns the ones that were freshly completed
|
||||||
|
/// - It internally marks them as "completed"
|
||||||
|
/// Important note: If you call this function, you must use the result, as those checkouts will not be returned the next time you call this function
|
||||||
|
///
|
||||||
|
/// - Returns: An array of checkout objects that have been successfully completed.
|
||||||
|
func check_status_of_checkouts_in_progress() async throws -> [String] {
|
||||||
|
var freshly_completed_checkouts: [String] = []
|
||||||
|
for checkout_id in self.checkout_ids_in_progress {
|
||||||
|
let checkout_info = try await self.fetch_ln_checkout_object(checkout_id: checkout_id)
|
||||||
|
if checkout_info?.is_all_good() == true {
|
||||||
|
freshly_completed_checkouts.append(checkout_id)
|
||||||
|
}
|
||||||
|
if checkout_info?.completed == true {
|
||||||
|
self.checkout_ids_in_progress.remove(checkout_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return freshly_completed_checkouts
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
/// This function checks the status of a specific checkout id with the server
|
||||||
|
/// You should use this result immediately, since it will internally be marked as handled
|
||||||
|
///
|
||||||
|
/// - Returns: true if this checkout is all good to go. false if not. nil if checkout was not found.
|
||||||
|
func check_and_mark_ln_checkout_is_good_to_go(checkout_id: String) async throws -> Bool? {
|
||||||
|
let checkout_info = try await self.fetch_ln_checkout_object(checkout_id: checkout_id)
|
||||||
|
if checkout_info?.completed == true {
|
||||||
|
self.checkout_ids_in_progress.remove(checkout_id) // Remove if from the list of checkouts in progress
|
||||||
|
}
|
||||||
|
return checkout_info?.is_all_good()
|
||||||
|
}
|
||||||
|
|
||||||
struct Account {
|
struct Account {
|
||||||
let pubkey: Pubkey
|
let pubkey: Pubkey
|
||||||
let created_at: Date
|
let created_at: Date
|
||||||
@@ -293,6 +353,44 @@ extension DamusPurple {
|
|||||||
let active: Bool
|
let active: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct LNCheckoutInfo: Codable {
|
||||||
|
// Note: Swift will decode a JSON full of extra fields into a Struct with only a subset of them, but not the other way around
|
||||||
|
// Therefore, to avoid compatibility concerns and complexity, we should only use the fields we need
|
||||||
|
// The ones we do not need yet will be left commented out until we need them.
|
||||||
|
let id: UUID
|
||||||
|
/*
|
||||||
|
let product_template_name: String
|
||||||
|
let verified_pubkey: String?
|
||||||
|
*/
|
||||||
|
let invoice: Invoice?
|
||||||
|
let completed: Bool
|
||||||
|
|
||||||
|
|
||||||
|
struct Invoice: Codable {
|
||||||
|
/*
|
||||||
|
let bolt11: String
|
||||||
|
let label: String
|
||||||
|
let connection_params: ConnectionParams
|
||||||
|
*/
|
||||||
|
let paid: Bool?
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct ConnectionParams: Codable {
|
||||||
|
let nodeid: String
|
||||||
|
let address: String
|
||||||
|
let rune: String
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicates whether this checkout is all good to go.
|
||||||
|
/// The checkout is good to go if it is marked as complete and the invoice has been successfully paid
|
||||||
|
/// - Returns: true if this checkout is all good to go. false otherwise
|
||||||
|
func is_all_good() -> Bool {
|
||||||
|
return self.completed == true && self.invoice?.paid == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate struct AccountUUIDInfo: Codable {
|
fileprivate struct AccountUUIDInfo: Codable {
|
||||||
let account_uuid: UUID
|
let account_uuid: UUID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ struct DamusPurpleVerifyNpubView: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
Task {
|
Task {
|
||||||
try await damus_state.purple.verify_npub_for_checkout(checkout_id: checkout_id)
|
try await damus_state.purple.verify_npub_for_checkout(checkout_id: checkout_id)
|
||||||
|
damus_state.purple.checkout_ids_in_progress.insert(checkout_id)
|
||||||
verified = true
|
verified = true
|
||||||
}
|
}
|
||||||
}, label: {
|
}, label: {
|
||||||
|
|||||||
@@ -21,20 +21,32 @@ extension DamusPurpleView {
|
|||||||
let subscribe: (Product) async throws -> Void
|
let subscribe: (Product) async throws -> Void
|
||||||
|
|
||||||
@State var show_manage_subscriptions = false
|
@State var show_manage_subscriptions = false
|
||||||
|
@State var subscription_purchase_loading = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch self.products {
|
if subscription_purchase_loading {
|
||||||
case .failed:
|
HStack(spacing: 10) {
|
||||||
PurpleViewPrimitives.ProductLoadErrorView()
|
Text(NSLocalizedString("Purchasing", comment: "Loading label indicating the purchase action is in progress"))
|
||||||
case .loaded(let products):
|
.foregroundStyle(.white)
|
||||||
if let purchased {
|
|
||||||
PurchasedView(purchased)
|
|
||||||
} else {
|
|
||||||
ProductsView(products)
|
|
||||||
}
|
|
||||||
case .loading:
|
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
|
.tint(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch self.products {
|
||||||
|
case .failed:
|
||||||
|
PurpleViewPrimitives.ProductLoadErrorView()
|
||||||
|
case .loaded(let products):
|
||||||
|
if let purchased {
|
||||||
|
PurchasedView(purchased)
|
||||||
|
} else {
|
||||||
|
ProductsView(products)
|
||||||
|
}
|
||||||
|
case .loading:
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +119,9 @@ extension DamusPurpleView {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
|
subscription_purchase_loading = true
|
||||||
try await subscribe(product)
|
try await subscribe(product)
|
||||||
|
subscription_purchase_loading = false
|
||||||
} catch {
|
} catch {
|
||||||
print(error.localizedDescription)
|
print(error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user