mirror of
git://jb55.com/damus
synced 2024-09-30 00:40:45 +00:00
purple: show welcome sheet after ln payment
Automatically show welcome sheet for people who completed a Lightning checkout but did not click on the "Continue" button Some people get confused in the last step of the lightning checkout and they do not click on the "Continue in app", which causes the welcome screen to not show up and the translation setting to not be setup. This ticket addresses this issue to ensure they get the welcome screen in such cases. This is how it works: - When they go through the mandatory checkout verification step, the verify screen registers that there is a checkout in progress (and which checkout is in progress) on the DamusPurple struct - Every time the app enters the foreground, it checks for that flag: - If there are no checkouts in progress, it does nothing - If there is one or multiple checkouts in progress, it checks the checkout status for those. If any checkout is freshly completed and successful, and the account is now active, it shows the welcome screen and onboarding - Once the welcome screen is shown, those checkouts are marked internally as handled (so that we only show it once) =========== Testing =========== PASS Damus: This commit Device: iPhone 13 Mini (real physical device) iOS: 17.3.1 damus-api: 044150fedddc5ba4135a80579e41e9c1c5743fc0 damus-website: af8089128159e25df31141be624b4090c66d6ddf Setup: - Local test Setup - Clean db before starting PART 1: LN flow without clicking on the link --------------------------------------------- Steps: 1. Go through the normal LN flow, EXCEPT at the last step. On the last step, instead of clicking "continue in the app", just switch to the app. 2. Ensure the welcome screen and onboarding shows up, and the purple screen updates to show account info. PASS 3. Switch out and into the app again. Welcome screen should NOT show up again. PASS 4. Close the app completely and open the app again. Purple welcome screen should NOT show up again. PASS PART 2: Normal LN flow --------------------------------------------- Steps: 1. Reset the entire test setup 2. Go through the normal LN flow (this time click on "continue in app"). The welcome screen should show up without issues or glitches. PASS PART 3: Crazy flow with multiple incomplete checkouts --------------------------------------------- (This is to simulate a confused user who accidentally opens multiple new checkouts, but in the end only completes one of them) Steps: 1. Reset the entire test setup 2. Open 5 LN checkouts and verify npub with all of them. 3. Only pay one of those checkouts (to make it more confusing, don't pay the first or last one, but a random one in between) 4. Go to the app directly (Do not use the link, just go directly to the app) Related: https://github.com/damus-io/damus/issues/1892 Changelog-Fixed: Fix welcome screen not showing if the user enters the app directly after a successful checkout without going through the link Closes: https://github.com/damus-io/damus/issues/2021 Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Link: 20240223212945.37384-2-daniel@daquino.me Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
parent
003348c103
commit
bdc811aa82
@ -29,6 +29,7 @@ enum Sheets: Identifiable {
|
||||
case user_status
|
||||
case onboardingSuggestions
|
||||
case purple(DamusPurpleURL)
|
||||
case purple_onboarding
|
||||
|
||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||
@ -50,6 +51,7 @@ enum Sheets: Identifiable {
|
||||
case .filter: return "filter"
|
||||
case .onboardingSuggestions: return "onboarding-suggestions"
|
||||
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!))
|
||||
case .purple(let purple_url):
|
||||
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
|
||||
case .purple_onboarding:
|
||||
DamusPurpleNewUserOnboardingView(damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
@ -343,12 +347,26 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
switch res {
|
||||
case .filter(let filt): self.open_search(filt: filt)
|
||||
case .profile(let pk): self.open_profile(pubkey: pk)
|
||||
case .event(let ev): self.open_event(ev: ev)
|
||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
||||
case .script(let data): self.open_script(data)
|
||||
case .purple(let purple_url): self.active_sheet = .purple(purple_url)
|
||||
case .filter(let filt): self.open_search(filt: filt)
|
||||
case .profile(let pk): self.open_profile(pubkey: pk)
|
||||
case .event(let ev): self.open_event(ev: ev)
|
||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
||||
case .script(let data): self.open_script(data)
|
||||
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 {
|
||||
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
|
||||
guard let damus_state else { return }
|
||||
|
@ -12,6 +12,7 @@ class DamusPurple: StoreObserverDelegate {
|
||||
let settings: UserSettingsStore
|
||||
let keypair: Keypair
|
||||
var storekit_manager: StoreKitManager
|
||||
var checkout_ids_in_progress: Set<String> = []
|
||||
|
||||
@MainActor
|
||||
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 {
|
||||
let pubkey: Pubkey
|
||||
let created_at: Date
|
||||
@ -293,6 +353,44 @@ extension DamusPurple {
|
||||
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 {
|
||||
let account_uuid: UUID
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ struct DamusPurpleVerifyNpubView: View {
|
||||
Button(action: {
|
||||
Task {
|
||||
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
|
||||
}
|
||||
}, label: {
|
||||
|
Loading…
Reference in New Issue
Block a user