diff --git a/Flipcash/Core/Controllers/ConversationController.swift b/Flipcash/Core/Controllers/ConversationController.swift index b107ce9ce..ee9a12784 100644 --- a/Flipcash/Core/Controllers/ConversationController.swift +++ b/Flipcash/Core/Controllers/ConversationController.swift @@ -875,6 +875,9 @@ final class ConversationController { } private func deliver(clientMessageID: UUID, text: String, to conversationID: ConversationID) async -> Bool { + // Captured before the send so success and failure report the same + // conversation kind; an unresolved conversation reports as Unknown. + let chatType = conversation(withID: conversationID)?.type do { let message = try await messaging.sendMessage(owner: owner, conversationID: conversationID, text: text, clientMessageID: clientMessageID) // Persist the row carrying its client id (the server echoes none) so the DB keeps the send's @@ -892,7 +895,7 @@ final class ConversationController { store.advanceLastActivity(to: message.date, in: conversationID) refreshFeedPreview(for: conversationID) persistConversation(conversationID) - Analytics.track(event: Analytics.ConversationEvent.sentMessage) + Analytics.sentMessage(chatType: chatType) return true } catch { store.markPending(clientMessageID: clientMessageID, status: .failed, in: conversationID) @@ -901,7 +904,7 @@ final class ConversationController { "error": "\(error)", ]) ErrorReporting.captureError(error, reason: "Failed to send conversation message") - Analytics.track(event: Analytics.ConversationEvent.sentMessage, error: error) + Analytics.sentMessage(chatType: chatType, error: error) return false } } diff --git a/Flipcash/Core/Screens/Main/ScanViewModel.swift b/Flipcash/Core/Screens/Main/ScanViewModel.swift index 02d4adb33..2733a68d5 100644 --- a/Flipcash/Core/Screens/Main/ScanViewModel.swift +++ b/Flipcash/Core/Screens/Main/ScanViewModel.swift @@ -80,6 +80,7 @@ class ScanViewModel { case .cash(let payload): didScanCash(payload) case .tip(let payload): + Analytics.track(event: Analytics.TipCardEvent.scanned) tipFlow.begin(userID: payload.userID) } } diff --git a/Flipcash/Core/Screens/Main/Tips/TipFlow.swift b/Flipcash/Core/Screens/Main/Tips/TipFlow.swift index 6c5a11782..248e0e619 100644 --- a/Flipcash/Core/Screens/Main/Tips/TipFlow.swift +++ b/Flipcash/Core/Screens/Main/Tips/TipFlow.swift @@ -179,6 +179,9 @@ final class TipFlow { } private func present(_ recipient: TipRecipient) { + // The card is resolved and about to show, whether reached from a scan or + // a deep link — the second step of the Scanned → Presented → Sent Tip funnel. + Analytics.track(event: Analytics.TipCardEvent.presented) selection = nil customAmount = nil submission = SendAmountViewModel( diff --git a/Flipcash/Core/Screens/Send/SendAmountViewModel.swift b/Flipcash/Core/Screens/Send/SendAmountViewModel.swift index 33976abd2..28515e944 100644 --- a/Flipcash/Core/Screens/Send/SendAmountViewModel.swift +++ b/Flipcash/Core/Screens/Send/SendAmountViewModel.swift @@ -195,6 +195,11 @@ final class SendAmountViewModel { return .failed } + // A payment into a tip DM reports as a tip; a contact DM is a plain + // cash send. This covers both the scanned-tipcard flow and the + // Send Cash action inside a tip thread, since both submit here. + let transferEvent: Analytics.TransferEvent = if case .tip = target { .sentTip } else { .sentCash } + do { try await sender.send( amount: amountToSend, @@ -202,10 +207,10 @@ final class SendAmountViewModel { to: recipient, chat: chatPaymentMetadata() ) - Analytics.transfer(event: .sentCash, exchangedFiat: amountToSend, grabTime: nil, successful: true, error: nil) + Analytics.transfer(event: transferEvent, exchangedFiat: amountToSend, grabTime: nil, successful: true, error: nil) return .success } catch { - Analytics.transfer(event: .sentCash, exchangedFiat: amountToSend, grabTime: nil, successful: false, error: error) + Analytics.transfer(event: transferEvent, exchangedFiat: amountToSend, grabTime: nil, successful: false, error: error) showSendError() return .failed } diff --git a/Flipcash/Utilities/Events.swift b/Flipcash/Utilities/Events.swift index 8c48f746c..09d89f474 100644 --- a/Flipcash/Utilities/Events.swift +++ b/Flipcash/Utilities/Events.swift @@ -37,6 +37,7 @@ extension Analytics { enum TransferEvent: String, AnalyticsEvent { case withdrawal = "Withdrawal" case sentCash = "Sent Cash" + case sentTip = "Sent Tip" case sendCashLink = "Send Cash Link" case receiveCashLink = "Receive Cash Link" case grabBill = "Grab Bill" @@ -61,6 +62,14 @@ extension Analytics { case sentMessage = "Sent Message" } + /// The scanned-tipcard funnel: a tipcard is scanned, resolves and is + /// presented, then a tip is sent (`TransferEvent.sentTip`). Names are + /// shared verbatim with Android. + enum TipCardEvent: String, AnalyticsEvent { + case scanned = "Tip Card Scanned" + case presented = "Tip Card Presented" + } + enum PhoneEvent: String, AnalyticsEvent { case entered = "Entered Phone Number" case verified = "Verified Phone Number" @@ -212,6 +221,31 @@ extension Analytics { } } +// MARK: - Conversation - + +extension Analytics { + /// A chat message send. `Chat Type` mirrors Android — Tip / Contact / + /// Unknown (a conversation not resolved locally yet). + static func sentMessage(chatType: ConversationType?, error: Error? = nil) { + track( + event: ConversationEvent.sentMessage, + properties: [.chatType: chatType.analyticsValue], + error: error + ) + } +} + +private extension Optional where Wrapped == ConversationType { + /// The `Chat Type` property value, shared verbatim with Android. + var analyticsValue: String { + switch self { + case .contactDm: "Contact" + case .tipDm: "Tip" + case .none: "Unknown" + } + } +} + // MARK: - Add Money - extension Analytics { @@ -377,6 +411,7 @@ extension Analytics { case fx = "Exchange Rate" case type = "Type" + case chatType = "Chat Type" case error = "Error" case url = "URL"