From 5a337b7e0e4e420bbca1ca3d0aaf3d3a6c13163a Mon Sep 17 00:00:00 2001 From: Raul Riera Date: Thu, 19 Feb 2026 12:59:51 -0500 Subject: [PATCH 1/4] feat: add analytics for token info and swap flows Track token info opens and swap transactions across the app. Adds analytics calls when token info is opened from deeplink, wallet, and Give (BalanceScreen, GiveViewModel). Introduces distinct SwapType cases (buyWithReserves, buyWithPhantom), surfaces an optional fee through the swap processing flow, and updates navigation paths to pass the appropriate swap type and fee (CurrencyInfoScreen, CurrencyBuyAmountScreen, CurrencySellAmountScreen, CurrencySellConfirmationScreen, SwapProcessingScreen). Implements transaction tracking in SwapProcessingViewModel (tokenPurchase/tokenSell) and adds new analytics events/properties and enum cases in Utilities/Events.swift. --- .../Core/Screens/Main/BalanceScreen.swift | 3 ++ .../Currency Info/CurrencyInfoScreen.swift | 2 +- .../CurrencyBuyAmountScreen.swift | 2 +- .../CurrencySellAmountScreen.swift | 4 +- .../CurrencySellConfirmationScreen.swift | 2 +- .../Currency Swap/CurrencySellViewModel.swift | 2 +- .../Currency Swap/SwapProcessingScreen.swift | 5 +- .../SwapProcessingViewModel.swift | 38 ++++++++++--- .../Core/Screens/Main/GiveViewModel.swift | 3 ++ Flipcash/Utilities/Events.swift | 54 +++++++++++++++++++ 10 files changed, 99 insertions(+), 16 deletions(-) diff --git a/Flipcash/Core/Screens/Main/BalanceScreen.swift b/Flipcash/Core/Screens/Main/BalanceScreen.swift index df3b66de..3ef8f46c 100644 --- a/Flipcash/Core/Screens/Main/BalanceScreen.swift +++ b/Flipcash/Core/Screens/Main/BalanceScreen.swift @@ -87,6 +87,7 @@ struct BalanceScreen: View { private func handlePendingCurrencyInfo() { guard let mint = session.pendingCurrencyInfoMint else { return } + Analytics.tokenInfoOpenedFromDeeplink(mint: mint) selectedMint = mint // Clear the pending mint @@ -162,6 +163,7 @@ struct BalanceScreen: View { if hasBalances { ForEach(currencyBalances) { balance in CurrencyBalanceRow(exchangedBalance: balance) { + Analytics.tokenInfoOpenedFromWallet(mint: balance.stored.mint) selectedMint = balance.stored.mint } } @@ -199,6 +201,7 @@ struct BalanceScreen: View { Divider() Button { + Analytics.tokenInfoOpenedFromWallet(mint: reservesBalance.stored.mint) selectedMint = reservesBalance.stored.mint } label: { HStack(spacing: 8) { diff --git a/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift b/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift index 526a0d8e..8f6d0aed 100644 --- a/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift @@ -329,7 +329,7 @@ struct CurrencyInfoScreen: View { .navigationDestinationCompat(item: Bindable(walletConnection).processing) { processing in SwapProcessingScreen( swapId: processing.swapId, - swapType: .buy, + swapType: .buyWithPhantom, mint: processing.mint, amount: processing.amount ) diff --git a/Flipcash/Core/Screens/Main/Currency Swap/CurrencyBuyAmountScreen.swift b/Flipcash/Core/Screens/Main/Currency Swap/CurrencyBuyAmountScreen.swift index c3c784c1..81d8a423 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/CurrencyBuyAmountScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/CurrencyBuyAmountScreen.swift @@ -46,7 +46,7 @@ struct CurrencyBuyAmountScreen: View { .navigationDestination(for: CurrencyBuyPath.self) { step in switch step { case .processing(let swapId, let mint, let amount): - SwapProcessingScreen(swapId: swapId, swapType: .buy, mint: mint, amount: amount) + SwapProcessingScreen(swapId: swapId, swapType: .buyWithReserves, mint: mint, amount: amount) .environment(\.dismissParentContainer, { dismissAction() }) diff --git a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift index 1d8e0f51..49fdf8ef 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift @@ -56,8 +56,8 @@ struct CurrencySellAmountScreen: View { dismissAction() }) } - case .processing(let swapId, let mint, let amount): - SwapProcessingScreen(swapId: swapId, swapType: .sell, mint: mint, amount: amount) + case .processing(let swapId, let mint, let amount, let fee): + SwapProcessingScreen(swapId: swapId, swapType: .sell, mint: mint, amount: amount, fee: fee) .environment(\.dismissParentContainer, { dismissAction() }) diff --git a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift index 88e422d7..5d67b338 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift @@ -92,7 +92,7 @@ struct CurrencySellConfirmationScreen: View { .navigationTitle("Confirm Sale") .onChange(of: viewModel.pendingSwapId) { _, swapId in if let swapId { - path.append(.processing(swapId: swapId, mint: mint, amount: viewModel.amountAfterFee)) + path.append(.processing(swapId: swapId, mint: mint, amount: viewModel.amountAfterFee, fee: viewModel.fee)) } } } diff --git a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift index 9c4ddf53..698940de 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift @@ -82,5 +82,5 @@ class CurrencySellViewModel: ObservableObject { enum CurrencySellPath: Hashable { case confirmation - case processing(swapId: SwapId, mint: PublicKey, amount: ExchangedFiat) + case processing(swapId: SwapId, mint: PublicKey, amount: ExchangedFiat, fee: ExchangedFiat) } diff --git a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift index cea39272..e575996a 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift @@ -20,12 +20,13 @@ struct SwapProcessingScreen: View { // MARK: - Init - - init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat) { + init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat, fee: ExchangedFiat? = nil) { _viewModel = State(wrappedValue: SwapProcessingViewModel( swapId: swapId, swapType: swapType, mint: mint, - amount: amount + amount: amount, + fee: fee )) } diff --git a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift index f014d236..cc12ad25 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift @@ -27,10 +27,9 @@ class SwapProcessingViewModel { return "This Will Take a Minute" case .success: if let exchangedFiat, let mintMetadata { - switch swapType { - case .buy: + if swapType.isBuy { return "\(exchangedFiat.converted.formatted()) of \(mintMetadata.name)" - case .sell: + } else { return "\(exchangedFiat.converted.formatted()) of USD Reserves" } } @@ -65,10 +64,9 @@ class SwapProcessingViewModel { var navigationTitle: String { switch displayState { case .processing: - switch swapType { - case .buy: + if swapType.isBuy { "Purchasing \(mintMetadata?.name ?? "")" - case .sell: + } else { "Selling \(mintMetadata?.name ?? "")" } case .success: @@ -92,14 +90,16 @@ class SwapProcessingViewModel { private let swapType: SwapType private let mint: PublicKey private let amount: ExchangedFiat + private let fee: ExchangedFiat? // MARK: - Init - - init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat) { + init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat, fee: ExchangedFiat? = nil) { self.swapId = swapId self.swapType = swapType self.mint = mint self.amount = amount + self.fee = fee } // MARK: - Actions - @@ -132,10 +132,13 @@ class SwapProcessingViewModel { switch metadata.state { case .finalized: setSwapDetails() + trackTransaction(successful: true) displayState = .success case .failed, .cancelled: + trackTransaction(successful: false) displayState = .failed case .unknown, .created, .funding, .funded, .submitting, .cancelling: + trackTransaction(successful: false) displayState = .failed } } catch is CancellationError { @@ -153,6 +156,17 @@ class SwapProcessingViewModel { private func setSwapDetails() { exchangedFiat = amount } + + private func trackTransaction(successful: Bool) { + switch swapType { + case .buyWithReserves: + Analytics.tokenPurchase(method: .tokenPurchaseWithReserves, exchangedFiat: amount, successful: successful) + case .buyWithPhantom: + Analytics.tokenPurchase(method: .tokenPurchaseWithPhantom, exchangedFiat: amount, successful: successful) + case .sell: + Analytics.tokenSell(exchangedFiat: amount, fee: fee, successful: successful) + } + } } // MARK: - DisplayState - @@ -168,6 +182,14 @@ extension SwapProcessingViewModel { // MARK: - SwapType - enum SwapType { - case buy + case buyWithReserves + case buyWithPhantom case sell + + var isBuy: Bool { + switch self { + case .buyWithReserves, .buyWithPhantom: true + case .sell: false + } + } } diff --git a/Flipcash/Core/Screens/Main/GiveViewModel.swift b/Flipcash/Core/Screens/Main/GiveViewModel.swift index 905d6977..ab377c78 100644 --- a/Flipcash/Core/Screens/Main/GiveViewModel.swift +++ b/Flipcash/Core/Screens/Main/GiveViewModel.swift @@ -161,6 +161,9 @@ class GiveViewModel: ObservableObject { private func presentDeposit() { depositMint = selectedBalance?.stored.mint + if let depositMint { + Analytics.tokenInfoOpenedFromGive(mint: depositMint) + } } // MARK: - Errors - diff --git a/Flipcash/Utilities/Events.swift b/Flipcash/Utilities/Events.swift index 5944b9bc..c9e68dbf 100644 --- a/Flipcash/Utilities/Events.swift +++ b/Flipcash/Utilities/Events.swift @@ -220,6 +220,49 @@ extension Analytics { } } +// MARK: - Token Info - + +extension Analytics { + static func tokenInfoOpenedFromDeeplink(mint: PublicKey) { + track(event: .tokenInfoOpenedFromDeeplink, properties: [.mint: mint.base58]) + } + + static func tokenInfoOpenedFromWallet(mint: PublicKey) { + track(event: .tokenInfoOpenedFromWallet, properties: [.mint: mint.base58]) + } + + static func tokenInfoOpenedFromGive(mint: PublicKey) { + track(event: .tokenInfoOpenedFromGive, properties: [.mint: mint.base58]) + } +} + +// MARK: - Token Transactions - + +extension Analytics { + static func tokenPurchase(method: Name, exchangedFiat: ExchangedFiat, successful: Bool, error: Error? = nil) { + var properties: [Property: AnalyticsValue] = [ + .state: successful ? String.success : String.failure, + .mint: exchangedFiat.mint.base58, + .fiat: exchangedFiat.converted.doubleValue, + .currency: exchangedFiat.rate.currency.rawValue, + ] + track(event: method, properties: properties, error: error) + } + + static func tokenSell(exchangedFiat: ExchangedFiat, fee: ExchangedFiat?, successful: Bool, error: Error? = nil) { + var properties: [Property: AnalyticsValue] = [ + .state: successful ? String.success : String.failure, + .mint: exchangedFiat.mint.base58, + .fiat: exchangedFiat.converted.doubleValue, + .currency: exchangedFiat.rate.currency.rawValue, + ] + if let fee { + properties[.fee] = fee.converted.doubleValue + } + track(event: .tokenSell, properties: properties, error: error) + } +} + // MARK: - Definitions - extension Analytics { @@ -262,6 +305,16 @@ extension Analytics { case walletCancel = "Wallet: Cancel" case cancelPendingPurchase = "Cancel Pending Purchase" + + // Token Info + case tokenInfoOpenedFromDeeplink = "Token Info: Opened From Deeplink" + case tokenInfoOpenedFromWallet = "Token Info: Opened From Wallet" + case tokenInfoOpenedFromGive = "Token Info: Opened From Give" + + // Token Transactions + case tokenPurchaseWithReserves = "Token Purchase With Reserves" + case tokenPurchaseWithPhantom = "Token Purchase With Phantom" + case tokenSell = "Token Sell" } } @@ -288,6 +341,7 @@ extension Analytics { case type = "Type" case error = "Error" + case fee = "Fee" } } From 0f1c4706ace68fb2fba5cea28dc85ec19a103b33 Mon Sep 17 00:00:00 2001 From: Raul Riera Date: Thu, 19 Feb 2026 13:21:52 -0500 Subject: [PATCH 2/4] refactor: analytics to typed events; remove swap fee Replace string-based analytics names with a typed AnalyticsEvent system and domain-specific enums (GeneralEvent, AccountEvent, ButtonEvent, TransferEvent, OnrampEvent, WalletEvent, TokenInfoEvent, TokenTransactionEvent). Change Analytics.track to accept AnalyticsEvent and update all call sites accordingly. Consolidate token info and token transaction tracking methods to use the new enums. Remove fee handling from the currency-swap processing flow (remove fee param/property from SwapProcessingViewModel/SwapProcessingScreen and related path cases), and update related screens/view models (CurrencySellAmountScreen, CurrencySellConfirmationScreen, CurrencySellViewModel). Update analytics calls across onboarding, onramp, wallet, session, give, and balance screens to the new typed API and rename several button/event identifiers. Also clean up Analytics property definitions (remove fee property) and unify property handling for track calls. --- .../Deep Links/Wallet/WalletConnection.swift | 8 +- .../Core/Screens/Main/BalanceScreen.swift | 6 +- .../CurrencySellAmountScreen.swift | 4 +- .../CurrencySellConfirmationScreen.swift | 2 +- .../Currency Swap/CurrencySellViewModel.swift | 2 +- .../Currency Swap/SwapProcessingScreen.swift | 5 +- .../SwapProcessingViewModel.swift | 11 +- .../Core/Screens/Main/GiveViewModel.swift | 2 +- .../Onboarding/OnboardingViewModel.swift | 16 +- .../Core/Screens/Onramp/OnrampViewModel.swift | 12 +- .../Core/Session/SessionAuthenticator.swift | 2 +- Flipcash/Utilities/Analytics.swift | 18 +- Flipcash/Utilities/Events.swift | 276 +++++++----------- 13 files changed, 152 insertions(+), 212 deletions(-) diff --git a/Flipcash/Core/Controllers/Deep Links/Wallet/WalletConnection.swift b/Flipcash/Core/Controllers/Deep Links/Wallet/WalletConnection.swift index db350be7..32f26f65 100644 --- a/Flipcash/Core/Controllers/Deep Links/Wallet/WalletConnection.swift +++ b/Flipcash/Core/Controllers/Deep Links/Wallet/WalletConnection.swift @@ -82,7 +82,7 @@ public final class WalletConnection { if let code = url.queryItemValue(for: "errorCode") { if code == "4001" { - Analytics.walletCancel() + Analytics.track(event: Analytics.WalletEvent.cancel) pendingSwap = nil if processing != nil { @@ -212,7 +212,7 @@ public final class WalletConnection { let errorCount = results.count - submittedSignatures.count if errorCount == 0 { - Analytics.walletTransactionsSubmitted() + Analytics.track(event: Analytics.WalletEvent.transactionsSubmitted) // If this was a swap transaction, notify server via buy() if let pending, let firstSignature = submittedSignatures.first, let self { @@ -250,7 +250,7 @@ public final class WalletConnection { } } } else { - Analytics.walletTransactionsFailed() + Analytics.track(event: Analytics.WalletEvent.transactionsFailed) await MainActor.run { self?.showSomethingWentWrongDialog() } @@ -298,7 +298,7 @@ public final class WalletConnection { URLQueryItem(name: "nonce", value: nonce) ] - Analytics.walletConnect() + Analytics.track(event: Analytics.WalletEvent.connect) openExternalWallet(c.url!) } diff --git a/Flipcash/Core/Screens/Main/BalanceScreen.swift b/Flipcash/Core/Screens/Main/BalanceScreen.swift index 3ef8f46c..f49fc839 100644 --- a/Flipcash/Core/Screens/Main/BalanceScreen.swift +++ b/Flipcash/Core/Screens/Main/BalanceScreen.swift @@ -87,7 +87,7 @@ struct BalanceScreen: View { private func handlePendingCurrencyInfo() { guard let mint = session.pendingCurrencyInfoMint else { return } - Analytics.tokenInfoOpenedFromDeeplink(mint: mint) + Analytics.tokenInfoOpened(from: .openedFromDeeplink, mint: mint) selectedMint = mint // Clear the pending mint @@ -163,7 +163,7 @@ struct BalanceScreen: View { if hasBalances { ForEach(currencyBalances) { balance in CurrencyBalanceRow(exchangedBalance: balance) { - Analytics.tokenInfoOpenedFromWallet(mint: balance.stored.mint) + Analytics.tokenInfoOpened(from: .openedFromWallet, mint: balance.stored.mint) selectedMint = balance.stored.mint } } @@ -201,7 +201,7 @@ struct BalanceScreen: View { Divider() Button { - Analytics.tokenInfoOpenedFromWallet(mint: reservesBalance.stored.mint) + Analytics.tokenInfoOpened(from: .openedFromWallet, mint: reservesBalance.stored.mint) selectedMint = reservesBalance.stored.mint } label: { HStack(spacing: 8) { diff --git a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift index 49fdf8ef..1d8e0f51 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellAmountScreen.swift @@ -56,8 +56,8 @@ struct CurrencySellAmountScreen: View { dismissAction() }) } - case .processing(let swapId, let mint, let amount, let fee): - SwapProcessingScreen(swapId: swapId, swapType: .sell, mint: mint, amount: amount, fee: fee) + case .processing(let swapId, let mint, let amount): + SwapProcessingScreen(swapId: swapId, swapType: .sell, mint: mint, amount: amount) .environment(\.dismissParentContainer, { dismissAction() }) diff --git a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift index 5d67b338..88e422d7 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellConfirmationScreen.swift @@ -92,7 +92,7 @@ struct CurrencySellConfirmationScreen: View { .navigationTitle("Confirm Sale") .onChange(of: viewModel.pendingSwapId) { _, swapId in if let swapId { - path.append(.processing(swapId: swapId, mint: mint, amount: viewModel.amountAfterFee, fee: viewModel.fee)) + path.append(.processing(swapId: swapId, mint: mint, amount: viewModel.amountAfterFee)) } } } diff --git a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift index 698940de..9c4ddf53 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/CurrencySellViewModel.swift @@ -82,5 +82,5 @@ class CurrencySellViewModel: ObservableObject { enum CurrencySellPath: Hashable { case confirmation - case processing(swapId: SwapId, mint: PublicKey, amount: ExchangedFiat, fee: ExchangedFiat) + case processing(swapId: SwapId, mint: PublicKey, amount: ExchangedFiat) } diff --git a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift index e575996a..cea39272 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingScreen.swift @@ -20,13 +20,12 @@ struct SwapProcessingScreen: View { // MARK: - Init - - init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat, fee: ExchangedFiat? = nil) { + init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat) { _viewModel = State(wrappedValue: SwapProcessingViewModel( swapId: swapId, swapType: swapType, mint: mint, - amount: amount, - fee: fee + amount: amount )) } diff --git a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift index cc12ad25..86f5baa2 100644 --- a/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift +++ b/Flipcash/Core/Screens/Main/Currency Swap/SwapProcessingViewModel.swift @@ -90,16 +90,13 @@ class SwapProcessingViewModel { private let swapType: SwapType private let mint: PublicKey private let amount: ExchangedFiat - private let fee: ExchangedFiat? - // MARK: - Init - - init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat, fee: ExchangedFiat? = nil) { + init(swapId: SwapId, swapType: SwapType, mint: PublicKey, amount: ExchangedFiat) { self.swapId = swapId self.swapType = swapType self.mint = mint self.amount = amount - self.fee = fee } // MARK: - Actions - @@ -160,11 +157,11 @@ class SwapProcessingViewModel { private func trackTransaction(successful: Bool) { switch swapType { case .buyWithReserves: - Analytics.tokenPurchase(method: .tokenPurchaseWithReserves, exchangedFiat: amount, successful: successful) + Analytics.tokenPurchase(method: .purchaseWithReserves, exchangedFiat: amount, successful: successful) case .buyWithPhantom: - Analytics.tokenPurchase(method: .tokenPurchaseWithPhantom, exchangedFiat: amount, successful: successful) + Analytics.tokenPurchase(method: .purchaseWithPhantom, exchangedFiat: amount, successful: successful) case .sell: - Analytics.tokenSell(exchangedFiat: amount, fee: fee, successful: successful) + Analytics.tokenSell(exchangedFiat: amount, successful: successful) } } } diff --git a/Flipcash/Core/Screens/Main/GiveViewModel.swift b/Flipcash/Core/Screens/Main/GiveViewModel.swift index ab377c78..9164578a 100644 --- a/Flipcash/Core/Screens/Main/GiveViewModel.swift +++ b/Flipcash/Core/Screens/Main/GiveViewModel.swift @@ -162,7 +162,7 @@ class GiveViewModel: ObservableObject { private func presentDeposit() { depositMint = selectedBalance?.stored.mint if let depositMint { - Analytics.tokenInfoOpenedFromGive(mint: depositMint) + Analytics.tokenInfoOpened(from: .openedFromGive, mint: depositMint) } } diff --git a/Flipcash/Core/Screens/Onboarding/OnboardingViewModel.swift b/Flipcash/Core/Screens/Onboarding/OnboardingViewModel.swift index ac9b1c59..f37c2c55 100644 --- a/Flipcash/Core/Screens/Onboarding/OnboardingViewModel.swift +++ b/Flipcash/Core/Screens/Onboarding/OnboardingViewModel.swift @@ -80,7 +80,7 @@ class OnboardingViewModel: ObservableObject { navigateToAccessKey() - Analytics.buttonTapped(name: .buttonCreateAccount) + Analytics.buttonTapped(name: .createAccount) } func saveToPhotosAction() { @@ -114,7 +114,7 @@ class OnboardingViewModel: ObservableObject { } } - Analytics.buttonTapped(name: .buttonSaveAccessKey) + Analytics.buttonTapped(name: .saveAccessKey) } func wroteDownAction() { @@ -129,7 +129,7 @@ class OnboardingViewModel: ObservableObject { try await self?.completeAccountCreation() } - Analytics.buttonTapped(name: .buttonWroteAccessKey) + Analytics.buttonTapped(name: .wroteAccessKey) }; .cancel() } @@ -234,7 +234,7 @@ class OnboardingViewModel: ObservableObject { completeOnboardingAndLogin() } - Analytics.buttonTapped(name: .buttonAllowCamera) + Analytics.buttonTapped(name: .allowCamera) } func cancelPendingPurchaseAction() { @@ -245,7 +245,7 @@ class OnboardingViewModel: ObservableObject { navigateToRoot() - Analytics.cancelPendingPurchase() + Analytics.track(event: Analytics.GeneralEvent.cancelPendingPurchase) } func allowPushPermissionsAction() { @@ -256,13 +256,13 @@ class OnboardingViewModel: ObservableObject { navigateToCameraAccessScreen() } - Analytics.buttonTapped(name: .buttonAllowPush) + Analytics.buttonTapped(name: .allowPush) } func skipPushPermissionsAction() { navigateToCameraAccessScreen() - Analytics.buttonTapped(name: .buttonSkipPush) + Analytics.buttonTapped(name: .skipPush) } // MARK: - Purchase - @@ -337,7 +337,7 @@ class OnboardingViewModel: ObservableObject { sessionAuthenticator.completeLogin(with: initializedAccount) - Analytics.track(event: .completeOnboarding) + Analytics.track(event: Analytics.GeneralEvent.completeOnboarding) } // MARK: - Pending Transactions - diff --git a/Flipcash/Core/Screens/Onramp/OnrampViewModel.swift b/Flipcash/Core/Screens/Onramp/OnrampViewModel.swift index 85a42956..92b0a9da 100644 --- a/Flipcash/Core/Screens/Onramp/OnrampViewModel.swift +++ b/Flipcash/Core/Screens/Onramp/OnrampViewModel.swift @@ -285,13 +285,13 @@ class OnrampViewModel: ObservableObject { } if origin.rawValue < Origin.phone.rawValue, !isPhoneVerified { - Analytics.onrampShowEnterPhone() + Analytics.track(event: Analytics.OnrampEvent.showEnterPhone) onrampPath.append(.enterPhoneNumber) return } if origin.rawValue < Origin.email.rawValue, !isEmailVerified { - Analytics.onrampShowEnterEmail() + Analytics.track(event: Analytics.OnrampEvent.showEnterEmail) onrampPath.append(.enterEmail) return } @@ -308,7 +308,7 @@ class OnrampViewModel: ObservableObject { isShowingVerificationFlow = false createOrder() } else { - Analytics.onrampShowVerificationInfo() + Analytics.track(event: Analytics.OnrampEvent.showVerificationInfo) isShowingVerificationFlow = true } } @@ -343,7 +343,7 @@ class OnrampViewModel: ObservableObject { func customAmountAction() { selectedPreset = nil isShowingAmountEntryScreen = true - Analytics.onrampEnterCustomAmount() + Analytics.track(event: Analytics.OnrampEvent.enterCustomAmount) } func customAmountEnteredAction() { @@ -394,7 +394,7 @@ class OnrampViewModel: ObservableObject { try await Task.delay(milliseconds: 500) onrampPath.append(.confirmPhoneNumberCode) - Analytics.onrampShowConfirmPhone() + Analytics.track(event: Analytics.OnrampEvent.showConfirmPhone) try await Task.delay(milliseconds: 500) } @@ -502,7 +502,7 @@ class OnrampViewModel: ObservableObject { try await Task.delay(milliseconds: 500) onrampPath.append(.confirmEmailCode) - Analytics.onrampShowConfirmEmail() + Analytics.track(event: Analytics.OnrampEvent.showConfirmEmail) try await Task.delay(milliseconds: 500) } diff --git a/Flipcash/Core/Session/SessionAuthenticator.swift b/Flipcash/Core/Session/SessionAuthenticator.swift index f5ab0efd..2b676949 100644 --- a/Flipcash/Core/Session/SessionAuthenticator.swift +++ b/Flipcash/Core/Session/SessionAuthenticator.swift @@ -76,7 +76,7 @@ final class SessionAuthenticator: ObservableObject { do { if let account = try await self?.initialize(using: keyAccount.mnemonic, isRegistration: false) { self?.completeLogin(with: account) - Analytics.autoLoginComplete() + Analytics.track(event: Analytics.GeneralEvent.autoLoginComplete) } } catch { self?.logout() diff --git a/Flipcash/Utilities/Analytics.swift b/Flipcash/Utilities/Analytics.swift index 6b06f3fe..2ec32c2c 100644 --- a/Flipcash/Utilities/Analytics.swift +++ b/Flipcash/Utilities/Analytics.swift @@ -13,6 +13,14 @@ import FlipcashCore typealias AnalyticsValue = MixpanelType +protocol AnalyticsEvent { + var eventName: String { get } +} + +extension AnalyticsEvent where Self: RawRepresentable { + var eventName: String { rawValue } +} + enum Analytics { static func initialize() { @@ -28,19 +36,19 @@ enum Analytics { } } - static func track(event: Name, properties: [Property: AnalyticsValue]? = nil, error: Error? = nil) { + static func track(event: some AnalyticsEvent, properties: [Property: AnalyticsValue]? = nil, error: Error? = nil) { var container: [String: AnalyticsValue] = [:] - + properties?.forEach { key, value in container[key.rawValue] = value } - + if let error { let swiftError = error as NSError container["Error"] = "\(swiftError.domain).\(error):\(swiftError.code)" } - - track(event.rawValue, properties: container) + + track(event.eventName, properties: container) } // static func track(_ action: Action, properties: [Property: AnalyticsValue]? = nil) { diff --git a/Flipcash/Utilities/Events.swift b/Flipcash/Utilities/Events.swift index c9e68dbf..093d3244 100644 --- a/Flipcash/Utilities/Events.swift +++ b/Flipcash/Utilities/Events.swift @@ -8,14 +8,74 @@ import Foundation import FlipcashCore -// MARK: - General - +// MARK: - Domain Event Enums - extension Analytics { - static func autoLoginComplete() { - track(event: .autoLoginComplete) + enum GeneralEvent: String, AnalyticsEvent { + case autoLoginComplete = "Auto-login complete" + case completeOnboarding = "Complete Onboarding" + case cancelPendingPurchase = "Cancel Pending Purchase" + } + + enum AccountEvent: String, AnalyticsEvent { + case createAccount = "Create Account" + } + + enum ButtonEvent: String, AnalyticsEvent { + case createAccount = "Button: Create Account" + case saveAccessKey = "Button: Save Access Key" + case wroteAccessKey = "Button: Wrote Access Key" + case allowCamera = "Button: Allow Camera" + case allowPush = "Button: Allow Push" + case skipPush = "Button: Skip Push" + } + + enum TransferEvent: String, AnalyticsEvent { + case withdrawal = "Withdrawal" + case sendCashLink = "Send Cash Link" + case receiveCashLink = "Receive Cash Link" + case grabBill = "Grab Bill" + case giveBill = "Give Bill" + } + + enum OnrampEvent: String, AnalyticsEvent { + case showVerificationInfo = "Onramp: Show Verification Info" + case showEnterPhone = "Onramp: Show Enter Phone" + case showConfirmPhone = "Onramp: Show Confirm Phone" + case showEnterEmail = "Onramp: Show Enter Email" + case showConfirmEmail = "Onramp: Show Confirm Email" + case presetSelected = "Onramp: Amount Selected" + case enterCustomAmount = "Onramp: Enter Custom Amount" + case invokePayment = "Onramp: Invoke Payment" + case invokePaymentCustom = "Onramp: Invoke Payment Custom" + case completed = "Onramp: Completed" + } + + enum WalletEvent: String, AnalyticsEvent { + case connect = "Wallet: Connect" + case requestAmount = "Wallet: Request Amount" + case transactionsSubmitted = "Wallet: Transactions Submitted" + case transactionsFailed = "Wallet: Transactions Failed" + case cancel = "Wallet: Cancel" + } + + enum TokenInfoEvent: String, AnalyticsEvent { + case openedFromDeeplink = "Token Info: Opened From Deeplink" + case openedFromWallet = "Token Info: Opened From Wallet" + case openedFromGive = "Token Info: Opened From Give" + } + + enum TokenTransactionEvent: String, AnalyticsEvent { + case purchaseWithReserves = "Token Purchase With Reserves" + case purchaseWithPhantom = "Token Purchase With Phantom" + case sell = "Token Sell" } - - static func buttonTapped(name: Name) { +} + +// MARK: - General - + +extension Analytics { + static func buttonTapped(name: ButtonEvent) { track(event: name) } } @@ -23,14 +83,9 @@ extension Analytics { // MARK: - Account - extension Analytics { - - static func cancelPendingPurchase() { - track(event: .cancelPendingPurchase) - } - static func createAccount(owner: PublicKey) { track( - event: .createAccount, + event: AccountEvent.createAccount, properties: [ .ownerPublicKey: owner.base58, ] @@ -41,12 +96,11 @@ extension Analytics { // MARK: - Cash Transfer - extension Analytics { - static func withdrawal(exchangedFiat: ExchangedFiat?, successful: Bool, error: Error?) { var properties: [Property: AnalyticsValue] = [ .state: successful ? String.success : String.failure, ] - + if let exchangedFiat { properties[.usdc] = exchangedFiat.underlying.doubleValue properties[.mint] = exchangedFiat.mint.base58 @@ -55,19 +109,19 @@ extension Analytics { properties[.fx] = exchangedFiat.rate.fx.analyticsValue properties[.currency] = exchangedFiat.rate.currency.rawValue } - + track( - event: .withdrawal, + event: TransferEvent.withdrawal, properties: properties, error: error ) } - - static func transfer(event: Name, exchangedFiat: ExchangedFiat?, grabTime: Double?, successful: Bool, error: Error?) { + + static func transfer(event: TransferEvent, exchangedFiat: ExchangedFiat?, grabTime: Double?, successful: Bool, error: Error?) { var properties: [Property: AnalyticsValue] = [ .state: successful ? String.success : String.failure, ] - + if let exchangedFiat { properties[.usdc] = exchangedFiat.underlying.doubleValue properties[.mint] = exchangedFiat.mint.base58 @@ -76,28 +130,28 @@ extension Analytics { properties[.fx] = exchangedFiat.rate.fx.analyticsValue properties[.currency] = exchangedFiat.rate.currency.rawValue } - + if let grabTime { properties[.grabTime] = grabTime } - + track( event: event, properties: properties, error: error ) } - - static func transfer(event: Name, fiat: Quarks?, successful: Bool, error: Error?) { + + static func transfer(event: TransferEvent, fiat: Quarks?, successful: Bool, error: Error?) { var properties: [Property: AnalyticsValue] = [ .state: successful ? String.success : String.failure, ] - + if let fiat { properties[.usdc] = fiat.doubleValue properties[.quarks] = fiat.quarks.analyticsValue } - + track( event: event, properties: properties, @@ -109,81 +163,45 @@ extension Analytics { // MARK: - Onramp - extension Analytics { - static func onrampOpenedFromSettings() { - track(event: .onrampOpenedFromSettings) - } - - static func onrampOpenedFromGive() { - track(event: .onrampOpenedFromGive) - } - - static func onrampOpenedFromBalance() { - track(event: .onrampOpenedFromBalance) - } - - static func onrampShowVerificationInfo() { - track(event: .onrampShowVerificationInfo) - } - - static func onrampShowEnterPhone() { - track(event: .onrampShowEnterPhone) - } - - static func onrampShowConfirmPhone() { - track(event: .onrampShowConfirmPhone) - } - - static func onrampShowEnterEmail() { - track(event: .onrampShowEnterEmail) - } - - static func onrampShowConfirmEmail() { - track(event: .onrampShowConfirmEmail) - } - static func onrampAmountPresetSelected(amount: Quarks) { var properties: [Property: AnalyticsValue] = [:] - + properties[.fiat] = amount.doubleValue properties[.currency] = amount.currencyCode.rawValue - - track(event: .onrampPresetSelected, properties: properties) - } - - static func onrampEnterCustomAmount() { - track(event: .onrampEnterCustomAmount) + + track(event: OnrampEvent.presetSelected, properties: properties) } - + static func onrampInvokePayment(amount: Quarks) { var properties: [Property: AnalyticsValue] = [:] - + properties[.fiat] = amount.doubleValue properties[.currency] = amount.currencyCode.rawValue - - track(event: .onrampInvokePayment, properties: properties) + + track(event: OnrampEvent.invokePayment, properties: properties) } - + static func onrampInvokePaymentCustom(amount: Quarks) { var properties: [Property: AnalyticsValue] = [:] - + properties[.fiat] = amount.doubleValue properties[.currency] = amount.currencyCode.rawValue - - track(event: .onrampInvokePaymentCustom, properties: properties) + + track(event: OnrampEvent.invokePaymentCustom, properties: properties) } - + static func onrampCompleted(amount: Quarks?, successful: Bool, error: Error?) { var properties: [Property: AnalyticsValue] = [ .state: successful ? String.success : String.failure, ] - + if let amount { properties[.fiat] = amount.doubleValue properties[.currency] = amount.currencyCode.rawValue } - + track( - event: .onrampCompleted, + event: OnrampEvent.completed, properties: properties, error: error ) @@ -193,54 +211,29 @@ extension Analytics { // MARK: - Wallet - extension Analytics { - - static func walletConnect() { - track(event: .walletConnect) - } - static func walletRequestAmount(amount: Quarks) { var properties: [Property: AnalyticsValue] = [:] - + properties[.fiat] = amount.doubleValue properties[.currency] = amount.currencyCode.rawValue - - track(event: .walletRequestAmount, properties: properties) - } - - static func walletTransactionsSubmitted() { - track(event: .walletTransactionsSubmitted) - } - - static func walletTransactionsFailed() { - track(event: .walletTransactionsFailed) - } - - static func walletCancel() { - track(event: .walletCancel) + + track(event: WalletEvent.requestAmount, properties: properties) } } // MARK: - Token Info - extension Analytics { - static func tokenInfoOpenedFromDeeplink(mint: PublicKey) { - track(event: .tokenInfoOpenedFromDeeplink, properties: [.mint: mint.base58]) - } - - static func tokenInfoOpenedFromWallet(mint: PublicKey) { - track(event: .tokenInfoOpenedFromWallet, properties: [.mint: mint.base58]) - } - - static func tokenInfoOpenedFromGive(mint: PublicKey) { - track(event: .tokenInfoOpenedFromGive, properties: [.mint: mint.base58]) + static func tokenInfoOpened(from event: TokenInfoEvent, mint: PublicKey) { + track(event: event, properties: [.mint: mint.base58]) } } // MARK: - Token Transactions - extension Analytics { - static func tokenPurchase(method: Name, exchangedFiat: ExchangedFiat, successful: Bool, error: Error? = nil) { - var properties: [Property: AnalyticsValue] = [ + static func tokenPurchase(method: TokenTransactionEvent, exchangedFiat: ExchangedFiat, successful: Bool, error: Error? = nil) { + let properties: [Property: AnalyticsValue] = [ .state: successful ? String.success : String.failure, .mint: exchangedFiat.mint.base58, .fiat: exchangedFiat.converted.doubleValue, @@ -249,78 +242,22 @@ extension Analytics { track(event: method, properties: properties, error: error) } - static func tokenSell(exchangedFiat: ExchangedFiat, fee: ExchangedFiat?, successful: Bool, error: Error? = nil) { - var properties: [Property: AnalyticsValue] = [ + static func tokenSell(exchangedFiat: ExchangedFiat, successful: Bool, error: Error? = nil) { + let properties: [Property: AnalyticsValue] = [ .state: successful ? String.success : String.failure, .mint: exchangedFiat.mint.base58, .fiat: exchangedFiat.converted.doubleValue, .currency: exchangedFiat.rate.currency.rawValue, ] - if let fee { - properties[.fee] = fee.converted.doubleValue - } - track(event: .tokenSell, properties: properties, error: error) + track(event: TokenTransactionEvent.sell, properties: properties, error: error) } } // MARK: - Definitions - -extension Analytics { - enum Name: String { - case createAccount = "Create Account" - case withdrawal = "Withdrawal" - case sendCashLink = "Send Cash Link" - case receiveCashLink = "Receive Cash Link" - case grabBill = "Grab Bill" - case giveBill = "Give Bill" - - case buttonCreateAccount = "Button: Create Account" - case buttonSaveAccessKey = "Button: Save Access Key" - case buttonWroteAccessKey = "Button: Wrote Access Key" - case buttonAllowCamera = "Button: Allow Camera" - case buttonAllowPush = "Button: Allow Push" - case buttonSkipPush = "Button: Skip Push" - - case autoLoginComplete = "Auto-login complete" - case completeOnboarding = "Complete Onboarding" - - case onrampOpenedFromSettings = "Onramp: Opened From Settings" - case onrampOpenedFromBalance = "Onramp: Opened From Balance" - case onrampOpenedFromGive = "Onramp: Opened From Give" - case onrampShowVerificationInfo = "Onramp: Show Verification Info" - case onrampShowEnterPhone = "Onramp: Show Enter Phone" - case onrampShowConfirmPhone = "Onramp: Show Confirm Phone" - case onrampShowEnterEmail = "Onramp: Show Enter Email" - case onrampShowConfirmEmail = "Onramp: Show Confirm Email" - case onrampPresetSelected = "Onramp: Amount Selected" - case onrampEnterCustomAmount = "Onramp: Enter Custom Amount" - case onrampInvokePayment = "Onramp: Invoke Payment" - case onrampInvokePaymentCustom = "Onramp: Invoke Payment Custom" - case onrampCompleted = "Onramp: Completed" - - case walletConnect = "Wallet: Connect" - case walletRequestAmount = "Wallet: Request Amount" - case walletTransactionsSubmitted = "Wallet: Transactions Submitted" - case walletTransactionsFailed = "Wallet: Transactions Failed" - case walletCancel = "Wallet: Cancel" - - case cancelPendingPurchase = "Cancel Pending Purchase" - - // Token Info - case tokenInfoOpenedFromDeeplink = "Token Info: Opened From Deeplink" - case tokenInfoOpenedFromWallet = "Token Info: Opened From Wallet" - case tokenInfoOpenedFromGive = "Token Info: Opened From Give" - - // Token Transactions - case tokenPurchaseWithReserves = "Token Purchase With Reserves" - case tokenPurchaseWithPhantom = "Token Purchase With Phantom" - case tokenSell = "Token Sell" - } -} - extension Analytics { enum Property: String { - + case id = "ID" case ownerPublicKey = "Owner Public Key" case autoCompleteCount = "Auto-complete count" @@ -328,7 +265,7 @@ extension Analytics { case result = "Result" case grabTime = "Grab Time" case time = "Time" - + case state = "State" case quarks = "Quarks" case usdc = "USDC" @@ -338,10 +275,9 @@ extension Analytics { case fx = "Exchange Rate" case animation = "Animation" case rendezvous = "Rendezvous" - + case type = "Type" case error = "Error" - case fee = "Fee" } } From 174a2ee81fa46266ac9fa07bdd8adbf65e4d62d2 Mon Sep 17 00:00:00 2001 From: Raul Riera Date: Thu, 19 Feb 2026 13:30:01 -0500 Subject: [PATCH 3/4] feat: add button tap analytics for buy and sell actions --- .../Screens/Main/Currency Info/CurrencyInfoScreen.swift | 3 +++ Flipcash/Utilities/Events.swift | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift b/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift index 8f6d0aed..ab90b90d 100644 --- a/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift @@ -313,6 +313,7 @@ struct CurrencyInfoScreen: View { if balance.quarks > 0 { CodeButton(style: .filledSecondary, title: "Sell") { + Analytics.buttonTapped(name: .sell) isShowingSellAmountEntry = true } } @@ -382,10 +383,12 @@ struct CurrencyInfoScreen: View { FundingSelectionSheet( reserveBalance: reserveBalance, onSelectReserves: { + Analytics.buttonTapped(name: .buyWithReserves) isShowingBuyAmountEntry = true isShowingFundingSelection = false }, onSelectPhantom: { + Analytics.buttonTapped(name: .buyWithPhantom) walletConnection.connectToPhantom() isShowingFundingSelection = false }, diff --git a/Flipcash/Utilities/Events.swift b/Flipcash/Utilities/Events.swift index 093d3244..f8814673 100644 --- a/Flipcash/Utilities/Events.swift +++ b/Flipcash/Utilities/Events.swift @@ -26,8 +26,11 @@ extension Analytics { case saveAccessKey = "Button: Save Access Key" case wroteAccessKey = "Button: Wrote Access Key" case allowCamera = "Button: Allow Camera" - case allowPush = "Button: Allow Push" - case skipPush = "Button: Skip Push" + case allowPush = "Button: Allow Push" + case skipPush = "Button: Skip Push" + case buyWithReserves = "Button: Buy With Reserves" + case buyWithPhantom = "Button: Buy With Phantom" + case sell = "Button: Sell" } enum TransferEvent: String, AnalyticsEvent { From feb2c7ea5a2e957f82cc508b0c17a6f67f933b2d Mon Sep 17 00:00:00 2001 From: Raul Riera Date: Thu, 19 Feb 2026 13:42:38 -0500 Subject: [PATCH 4/4] feat: add share token info button analytics --- .../Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift | 1 + Flipcash/Utilities/Events.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift b/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift index ab90b90d..4f41ccd8 100644 --- a/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift +++ b/Flipcash/Core/Screens/Main/Currency Info/CurrencyInfoScreen.swift @@ -157,6 +157,7 @@ struct CurrencyInfoScreen: View { if !isUSDF { ToolbarItem(placement: .navigationBarTrailing) { Button { + Analytics.buttonTapped(name: .shareTokenInfo) let url = URL(string: "https://app.flipcash.com/token/\(mint.base58)")! ShareSheet.present(url: url) } label: { diff --git a/Flipcash/Utilities/Events.swift b/Flipcash/Utilities/Events.swift index f8814673..566f7401 100644 --- a/Flipcash/Utilities/Events.swift +++ b/Flipcash/Utilities/Events.swift @@ -31,6 +31,7 @@ extension Analytics { case buyWithReserves = "Button: Buy With Reserves" case buyWithPhantom = "Button: Buy With Phantom" case sell = "Button: Sell" + case shareTokenInfo = "Button: Share Token Info" } enum TransferEvent: String, AnalyticsEvent {