diff --git a/Mixin.xcodeproj/project.pbxproj b/Mixin.xcodeproj/project.pbxproj index 6ae1c2df40..cfbb5a5fba 100644 --- a/Mixin.xcodeproj/project.pbxproj +++ b/Mixin.xcodeproj/project.pbxproj @@ -992,6 +992,7 @@ 944F547F2E45F9A20064A9A2 /* WatchingAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944F547E2E45F99F0064A9A2 /* WatchingAddresses.swift */; }; 94509BC12DDDFC5D00DC6A43 /* RefreshWeb3TokenJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94509BC02DDDFC5800DC6A43 /* RefreshWeb3TokenJob.swift */; }; 94509D772DE086E100DC6A43 /* IAPTransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94509D762DE086D800DC6A43 /* IAPTransactionObserver.swift */; }; + 9450EB642FC8964E003534FF /* UserOperationAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9450EB632FC89647003534FF /* UserOperationAnalytics.swift */; }; 94510FBB2C85673C00ACD972 /* ReloadGlobalMarketJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94510FBA2C85673C00ACD972 /* ReloadGlobalMarketJob.swift */; }; 94510FCE2C8742BC00ACD972 /* MarketTokenSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94510FCD2C8742BC00ACD972 /* MarketTokenSelectorViewController.swift */; }; 94510FD42C8743AA00ACD972 /* MarketTokenSelectorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94510FD22C8743AA00ACD972 /* MarketTokenSelectorCell.swift */; }; @@ -2893,6 +2894,7 @@ 944F547E2E45F99F0064A9A2 /* WatchingAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchingAddresses.swift; sourceTree = ""; }; 94509BC02DDDFC5800DC6A43 /* RefreshWeb3TokenJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshWeb3TokenJob.swift; sourceTree = ""; }; 94509D762DE086D800DC6A43 /* IAPTransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPTransactionObserver.swift; sourceTree = ""; }; + 9450EB632FC89647003534FF /* UserOperationAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserOperationAnalytics.swift; sourceTree = ""; }; 94510FBA2C85673C00ACD972 /* ReloadGlobalMarketJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadGlobalMarketJob.swift; sourceTree = ""; }; 94510FCD2C8742BC00ACD972 /* MarketTokenSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTokenSelectorViewController.swift; sourceTree = ""; }; 94510FD22C8743AA00ACD972 /* MarketTokenSelectorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTokenSelectorCell.swift; sourceTree = ""; }; @@ -6639,6 +6641,7 @@ 948CFAE92F2CB4B200BC34C3 /* QRCodeDetector.swift */, 94EC9CD32F2C7CC000B3CD82 /* VideoCaptureDevice.swift */, 9404D76F2FB1A72600F5563A /* SignPosition.swift */, + 9450EB632FC89647003534FF /* UserOperationAnalytics.swift */, ); path = Service; sourceTree = ""; @@ -8526,6 +8529,7 @@ 7B2AFEE220FE022C00C747BB /* MXMFastURLDetector.m in Sources */, DF1F277C21A53585009A74C6 /* BackupViewController.swift in Sources */, 942FD9342D3A70DB004DCC4C /* TradeOrderHeaderCell.swift in Sources */, + 9450EB642FC8964E003534FF /* UserOperationAnalytics.swift in Sources */, 945FFC7A2C3818630013DC51 /* RouteAPI.swift in Sources */, ABCB87DD23F18A3D00780E60 /* PermissionsViewController.swift in Sources */, 9443A9212CA6BB3D0030A636 /* MarketAlertCoinPickerViewController.swift in Sources */, diff --git a/Mixin/AppDelegate.swift b/Mixin/AppDelegate.swift index 4f79986a99..333bee81f1 100644 --- a/Mixin/AppDelegate.swift +++ b/Mixin/AppDelegate.swift @@ -41,6 +41,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { assertionFailure("Missing AppsFlyer key") } AppsFlyerLib.shared().appleAppID = appStoreAppID + AppsFlyerLib.shared().delegate = self AppGroupUserDefaults.migrateIfNeeded() updateImageManagerConfig() _ = ReachabilityManger.shared @@ -433,3 +434,35 @@ extension AppDelegate { } } + +extension AppDelegate : AppsFlyerLibDelegate { + + // Handle Organic/Non-organic installation + func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) { + guard let status = conversionInfo["af_status"] as? String else { + return + } + guard UIApplication.shared.isProtectedDataAvailable else { + return + } + guard LoginManager.shared.isLoggedIn else { + return + } + + reporter.updateUserProperty(key: "af_source", value: status) + if status == "Non-Organic" { + if let mediaSource = conversionInfo["media_source"] as? String, !mediaSource.isEmpty { + reporter.updateUserProperty(key: "af_media_source", value: mediaSource) + } + + if let campaign = conversionInfo["campaign"] as? String, !campaign.isEmpty { + reporter.updateUserProperty(key: "af_campaign", value: campaign) + } + } + } + + func onConversionDataFail(_ error: any Error) { + Logger.general.error(category: "AppsFlyer", message: "onConversionDataFail: \(error)") + } + +} diff --git a/Mixin/Service/MainAppReporter.swift b/Mixin/Service/MainAppReporter.swift index 6cec5a89da..1d22fffd76 100644 --- a/Mixin/Service/MainAppReporter.swift +++ b/Mixin/Service/MainAppReporter.swift @@ -2,6 +2,7 @@ import UIKit import FirebaseCore import FirebaseAnalytics import FirebaseCrashlytics +import AppsFlyerLib import MixinServices final class MainAppReporter: Reporter { @@ -14,6 +15,7 @@ final class MainAppReporter: Reporter { "IdentityNumber": account.identityNumber ]) Analytics.setUserID(account.userID) + AppsFlyerLib.shared().customerUserID = account.userID } override func report(error: Error) { @@ -24,6 +26,35 @@ final class MainAppReporter: Reporter { override func report(event: Event, tags: [String: String]? = nil) { super.report(event: event, tags: tags) Analytics.logEvent(event.rawValue, parameters: tags) + + switch event { + case .signUpStart, + .buyStart, + .tradeSpotStart, + .tradeSpotEnd, + .tradePerpsOpenPositionStart, + .tradePerpsOpenPositionEnd, + .receiveStart, + .receiveEnd, + .sendStart, + .sendEnd: + AppsFlyerLib.shared().logEvent(event.rawValue, withValues: tags) + AppsFlyerLib.shared().start() + case .signUpAccountCreated: + AppsFlyerLib.shared().logEvent(event.rawValue, withValues: tags) + if let appInstanceID = Analytics.appInstanceID() { + AppsFlyerLib.shared().customData = ["app_instance_id": appInstanceID] + } + AppsFlyerLib.shared().start() + case .loginEnd: + AppsFlyerLib.shared().logEvent(AFEventLogin, withValues: tags) + AppsFlyerLib.shared().start() + case .signUpEnd: + AppsFlyerLib.shared().logEvent(AFEventCompleteRegistration, withValues: tags) + AppsFlyerLib.shared().start() + default: + break + } } override func updateUserProperties(_ properties: UserProperty, account: Account? = nil) { @@ -63,4 +94,9 @@ final class MainAppReporter: Reporter { } } + override func updateUserProperty(key: String, value: String) { + super.updateUserProperty(key: key, value: value) + Analytics.setUserProperty(value, forName: key) + } + } diff --git a/Mixin/Service/UserOperationAnalytics.swift b/Mixin/Service/UserOperationAnalytics.swift new file mode 100644 index 0000000000..a5419c498b --- /dev/null +++ b/Mixin/Service/UserOperationAnalytics.swift @@ -0,0 +1,32 @@ +import Foundation + +enum UserOperationAnalytics { + + enum TradeSource: String { + case walletHome = "wallet_home" + case moreExplore = "more_explore" + case appCard = "app_card" + case marketDetail = "market_detail" + case tradeDetail = "trade_detail" + case assetDetail = "asset_detail" + case perpsMarginInput = "perps_margin_input" + case transfer = "transfer" + case withdraw = "withdraw" + case scheme = "scheme" + } + + static var tradeSource: TradeSource? + +} + +extension UserOperationAnalytics { + + enum AddMobileNumberSource: String { + case recoveryKitGuide = "recovery_kit_guide" + case buyGuide = "buy_guide" + case settings = "settings" + } + + static var addMobileNumberSource: AddMobileNumberSource? + +} diff --git a/Mixin/UserInterface/Controllers/Home/ExploreViewController.swift b/Mixin/UserInterface/Controllers/Home/ExploreViewController.swift index abb9121ff0..ebc452688e 100644 --- a/Mixin/UserInterface/Controllers/Home/ExploreViewController.swift +++ b/Mixin/UserInterface/Controllers/Home/ExploreViewController.swift @@ -125,6 +125,7 @@ final class ExploreViewController: UIViewController, AssetChangeAccountRecoveryC reporter.report(event: .buyStart, tags: ["wallet": "main", "source": "explore"]) case .trade: BadgeManager.shared.setHasViewed(identifier: .trade) + UserOperationAnalytics.tradeSource = .moreExplore let trade = TradeViewController( wallet: .privacy, trading: nil, @@ -137,7 +138,6 @@ final class ExploreViewController: UIViewController, AssetChangeAccountRecoveryC } withAccountRecoveryChecked { [weak self] in self?.navigationController?.pushViewController(trade, animated: true) - reporter.report(event: .tradeStart, tags: ["wallet": "main", "source": "explore"]) } case .membership: if let membership = LoginManager.shared.account?.membership, let plan = membership.plan { diff --git a/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift b/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift index 4dae350ee7..1adf8faea8 100644 --- a/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/CheckSessionEnvironmentViewController.swift @@ -28,8 +28,6 @@ final class CheckSessionEnvironmentViewController: UIViewController { private var account: Account private var isAccountFresh: Bool - private var isUsernameJustInitialized = false - private var initialBots: [String] { if Locale.preferredLanguages.first?.hasPrefix("zh-Hans") ?? false { [ @@ -93,7 +91,6 @@ final class CheckSessionEnvironmentViewController: UIViewController { reload(content: clockSkew) } else if account.fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Logger.login.info(category: "CheckSessionEnvironment", message: "Create username") - isUsernameJustInitialized = true let username = UsernameViewController() let navigationController = GeneralAppearanceNavigationController(rootViewController: username) reload(content: navigationController) @@ -109,12 +106,13 @@ final class CheckSessionEnvironmentViewController: UIViewController { reload(content: upgrade) } else if !SignalLoadingViewController.isLoaded { Logger.login.info(category: "CheckSessionEnvironment", message: "Load Signal") - let signalLoading = SignalLoadingViewController(isUsernameJustInitialized: isUsernameJustInitialized) + let signalLoading = SignalLoadingViewController() signalLoading.onFinished = { [weak self] in guard let self else { return } - if !self.isUsernameJustInitialized { + + if !AppGroupUserDefaults.isSigningUp { Logger.login.info(category: "CheckSessionEnvironment", message: "Sync contacts") ContactAPI.syncContacts() } @@ -156,9 +154,7 @@ final class CheckSessionEnvironmentViewController: UIViewController { } } else { Logger.login.info(category: "CheckSessionEnvironment", message: "Create PIN") - let tip = TIPNavigationController(intent: .create) - tip.redirectsToWalletTabOnFinished = true - root = tip + root = TIPNavigationController(intent: .create) } AppDelegate.current.mainWindow.rootViewController = root } diff --git a/Mixin/UserInterface/Controllers/Login/CreateAccountIntroductionViewController.swift b/Mixin/UserInterface/Controllers/Login/CreateAccountIntroductionViewController.swift index ddc1631908..ca8326a283 100644 --- a/Mixin/UserInterface/Controllers/Login/CreateAccountIntroductionViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/CreateAccountIntroductionViewController.swift @@ -8,6 +8,18 @@ final class CreateAccountIntroductionViewController: UIViewController { @IBOutlet weak var continueButton: UIButton! @IBOutlet weak var footerTextView: IntroTextView! + private let analyticSource: String + + init(analyticSource: String) { + self.analyticSource = analyticSource + let nib = R.nib.createAccountIntroductionView + super.init(nibName: nib.name, bundle: nib.bundle) + } + + required init?(coder: NSCoder) { + fatalError("Storyboard not supported") + } + override func viewDidLoad() { super.viewDidLoad() titleLabel.text = R.string.localizable.create_your_account() @@ -56,7 +68,7 @@ final class CreateAccountIntroductionViewController: UIViewController { guard let navigationController else { return } - presentingViewController.dismiss(animated: true) { + presentingViewController.dismiss(animated: true) { [analyticSource] in let mnemonics: MixinMnemonics? = if let entropy = AppGroupKeychain.mnemonics { try? MixinMnemonics(entropy: entropy) } else { @@ -69,7 +81,7 @@ final class CreateAccountIntroductionViewController: UIViewController { viewControllers.append(next) navigationController.setViewControllers(viewControllers, animated: true) Logger.login.info(category: "CreateAccountIntro", message: "Sign up start") - reporter.report(event: .signUpStart) + reporter.report(event: .signUpStart, tags: ["source": analyticSource]) } } diff --git a/Mixin/UserInterface/Controllers/Login/LoginPINValidationViewController.swift b/Mixin/UserInterface/Controllers/Login/LoginPINValidationViewController.swift index 31a40301c0..4d4ede8663 100644 --- a/Mixin/UserInterface/Controllers/Login/LoginPINValidationViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/LoginPINValidationViewController.swift @@ -53,23 +53,18 @@ final class LoginPINValidationViewController: FullscreenPINValidationViewControl try await TIP.registerDefaultCommonWalletIfNeeded(pin: pin) AppGroupUserDefaults.Wallet.lastPINVerifiedDate = Date() - // This view appears in the following two scenarios: - // 1. The user is already signed in, but has not registered Safe or Common Wallet. - // 2. The user has just signed up or just signed in. - // - // Use the value of `loginPINValidated` to determine which scenario is being handled: - // - For scenario 1, the default tab should be Chat. - // - For scenario 2, the default tab should be Wallet. - let isNewLogin = !AppGroupUserDefaults.User.loginPINValidated - AppGroupUserDefaults.User.loginPINValidated = true await MainActor.run { + if AppGroupUserDefaults.isSigningUp { + reporter.report(event: .signUpEnd) + } else { + reporter.report(event: .loginEnd) + } Logger.login.info(category: "LoginPINValidation", message: "Validated") Logger.redirectLogsToLogin = false AppDelegate.current.mainWindow.rootViewController = HomeContainerViewController( - initialTab: isNewLogin ? .wallet : .chat + initialTab: .wallet ) - reporter.report(event: .loginEnd) } } catch MixinAPIResponseError.malformedPin { Logger.login.error(category: "LoginPINValidation", message: "malformedPin...hasPIN:\(account.hasPIN)...hasSafe:\(account.hasSafe)") diff --git a/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift b/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift index 6b4de0cd3c..6361683415 100644 --- a/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/LoginVerificationCodeViewController.swift @@ -101,7 +101,7 @@ class LoginVerificationCodeViewController: VerificationCodeViewController, Login } switch result { case let .success(account): - if let error = self.login(account: account, sessionKey: sessionKey) { + if let error = self.login(account: account, signingUp: false, sessionKey: sessionKey) { self.handleVerificationCodeError(error) } case let .failure(error): diff --git a/Mixin/UserInterface/Controllers/Login/LoginWithMnemonicViewController.swift b/Mixin/UserInterface/Controllers/Login/LoginWithMnemonicViewController.swift index 0dc809fbda..675263da63 100644 --- a/Mixin/UserInterface/Controllers/Login/LoginWithMnemonicViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/LoginWithMnemonicViewController.swift @@ -268,16 +268,25 @@ extension LoginWithMnemonicViewController { } switch result { case let .success(account): + let isSigningUp: Bool switch self.action { case .signIn(let mnemonics): + isSigningUp = false if account.isAnonymous { AppGroupKeychain.mnemonics = mnemonics.entropy Logger.login.info(category: "MnemonicLogin", message: "Mnemonics saved to Keychain") } case .signUp: - break + isSigningUp = true + reporter.registerUserInformation(account: account) + reporter.report(event: .signUpAccountCreated) } - if let error = self.login(account: account, sessionKey: context.sessionKey) { + let error = self.login( + account: account, + signingUp: isSigningUp, + sessionKey: context.sessionKey + ) + if let error { Logger.login.error(category: "MnemonicLogin", message: "\(error)") self.showError(error.localizedDescription) } diff --git a/Mixin/UserInterface/Controllers/Login/Model/LoginAccountHandler.swift b/Mixin/UserInterface/Controllers/Login/Model/LoginAccountHandler.swift index 883fc4a227..233372e843 100644 --- a/Mixin/UserInterface/Controllers/Login/Model/LoginAccountHandler.swift +++ b/Mixin/UserInterface/Controllers/Login/Model/LoginAccountHandler.swift @@ -3,12 +3,16 @@ import WebKit import MixinServices protocol LoginAccountHandler { - func login(account: Account, sessionKey: Ed25519PrivateKey) -> MixinAPIError? + func login(account: Account, signingUp: Bool, sessionKey: Ed25519PrivateKey) -> MixinAPIError? } extension LoginAccountHandler where Self: UIViewController { - func login(account: Account, sessionKey: Ed25519PrivateKey) -> MixinAPIError? { + func login( + account: Account, + signingUp: Bool, + sessionKey: Ed25519PrivateKey + ) -> MixinAPIError? { guard !account.pinToken.isEmpty, let remotePublicKey = Data(base64Encoded: account.pinToken), @@ -21,6 +25,7 @@ extension LoginAccountHandler where Self: UIViewController { return .invalidServerPinToken } Logger.login.info(category: "Login", message: "Got account: \(account.userID), has_pin: \(account.hasPIN), has_safe: \(account.hasSafe), tip_key: \(account.tipKey?.count ?? -1)") + AppGroupUserDefaults.isSigningUp = signingUp AppGroupKeychain.sessionSecret = sessionKey.rawRepresentation AppGroupKeychain.pinToken = pinToken if !account.isAnonymous { diff --git a/Mixin/UserInterface/Controllers/Login/OnboardingViewController.swift b/Mixin/UserInterface/Controllers/Login/OnboardingViewController.swift index 741136a4d3..403430ce11 100644 --- a/Mixin/UserInterface/Controllers/Login/OnboardingViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/OnboardingViewController.swift @@ -132,7 +132,7 @@ final class OnboardingViewController: UIViewController { } @IBAction func signUp(_ sender: Any) { - let intro = CreateAccountIntroductionViewController() + let intro = CreateAccountIntroductionViewController(analyticSource: "landing") present(intro, animated: true) } diff --git a/Mixin/UserInterface/Controllers/Login/RecoveryContactLoginVerificationCodeViewController.swift b/Mixin/UserInterface/Controllers/Login/RecoveryContactLoginVerificationCodeViewController.swift index 953a7401b2..b6ff14e0ae 100644 --- a/Mixin/UserInterface/Controllers/Login/RecoveryContactLoginVerificationCodeViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/RecoveryContactLoginVerificationCodeViewController.swift @@ -31,7 +31,9 @@ final class RecoveryContactLoginVerificationCodeViewController: LoginVerificatio switch result { case let .success(account): HomeViewController.showChangePhoneNumberTips = true - if let self, let error = self.login(account: account, sessionKey: sessionKey) { + if let self, + let error = self.login(account: account, signingUp: false, sessionKey: sessionKey) + { self.handleVerificationCodeError(error) } case let .failure(error): diff --git a/Mixin/UserInterface/Controllers/Login/SignInWithMnemonicsViewController.swift b/Mixin/UserInterface/Controllers/Login/SignInWithMnemonicsViewController.swift index 453951a75b..e43e34d16e 100644 --- a/Mixin/UserInterface/Controllers/Login/SignInWithMnemonicsViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/SignInWithMnemonicsViewController.swift @@ -78,7 +78,9 @@ final class SignInWithMnemonicsViewController: InputMnemonicsViewController { override func confirm(_ sender: Any) { if phrasesCount == nil { - let intro = CreateAccountIntroductionViewController() + let intro = CreateAccountIntroductionViewController( + analyticSource: "login_mnemonic_phrase" + ) present(intro, animated: true) } else { do { @@ -188,6 +190,11 @@ final class SignInWithMnemonicsViewController: InputMnemonicsViewController { return } input(phrases: phrases) + let bottomOffset = CGPoint( + x: scrollView.contentOffset.x, + y: max(0, scrollView.contentSize.height - scrollView.frame.height) + ) + scrollView.setContentOffset(bottomOffset, animated: true) } @objc private func emptyPhrases(_ sender: Any) { diff --git a/Mixin/UserInterface/Controllers/Login/SignInWithMobileNumberViewController.swift b/Mixin/UserInterface/Controllers/Login/SignInWithMobileNumberViewController.swift index ca868869bc..62ac2bc3d5 100644 --- a/Mixin/UserInterface/Controllers/Login/SignInWithMobileNumberViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/SignInWithMobileNumberViewController.swift @@ -80,7 +80,7 @@ final class SignInWithMobileNumberViewController: MobileNumberViewController { } @objc private func signup(_ sender: Any) { - let intro = CreateAccountIntroductionViewController() + let intro = CreateAccountIntroductionViewController(analyticSource: "login_start") present(intro, animated: true) } @@ -153,7 +153,16 @@ extension SignInWithMobileNumberViewController { fallthrough } default: - reporter.report(event: .errorSessionVerifications, tags: ["source": "sign_up"]) + var tags = ["type": "phone"] + if error.isServerErrorResponse { + tags["error_type"] = "server_error" + } else if error.isClientErrorResponse { + tags["error_type"] = "client_error" + } + if let statusCode = self.request?.response?.statusCode { + tags["error_code"] = "\(statusCode)" + } + reporter.report(event: .errorSessionVerifications, tags: tags) var userInfo: [String: String] = [:] userInfo["error"] = "\(error)" if let requestId = self.request?.response?.value(forHTTPHeaderField: "x-request-id") { diff --git a/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift b/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift index bcfc0dd44d..9bccf28f4c 100644 --- a/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift +++ b/Mixin/UserInterface/Controllers/Login/SignalLoadingViewController.swift @@ -9,17 +9,6 @@ final class SignalLoadingViewController: LoginLoadingViewController { && AppGroupUserDefaults.User.isCircleSynchronized } - let isUsernameJustInitialized: Bool - - init(isUsernameJustInitialized: Bool) { - self.isUsernameJustInitialized = isUsernameJustInitialized - super.init() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - var onFinished: (() -> Void)? override func viewDidLoad() { @@ -43,7 +32,7 @@ final class SignalLoadingViewController: LoginLoadingViewController { } } - if isUsernameJustInitialized { + if AppGroupUserDefaults.isSigningUp { reporter.report(event: .signUpSignalInit) } else { reporter.report(event: .loginSignalInit) diff --git a/Mixin/UserInterface/Controllers/Setting/Mobile Number/AddMobileNumberViewController.swift b/Mixin/UserInterface/Controllers/Setting/Mobile Number/AddMobileNumberViewController.swift index 15ecf20137..9161219992 100644 --- a/Mixin/UserInterface/Controllers/Setting/Mobile Number/AddMobileNumberViewController.swift +++ b/Mixin/UserInterface/Controllers/Setting/Mobile Number/AddMobileNumberViewController.swift @@ -44,6 +44,7 @@ extension AddMobileNumberViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) + UserOperationAnalytics.addMobileNumberSource = .settings let introduction = MobileNumberIntroductionViewController(action: .add) navigationController?.pushViewController(introduction, animated: true) } diff --git a/Mixin/UserInterface/Controllers/Setting/Mobile Number/MobileNumberIntroductionViewController.swift b/Mixin/UserInterface/Controllers/Setting/Mobile Number/MobileNumberIntroductionViewController.swift index 6501bdd116..ee67d14ac8 100644 --- a/Mixin/UserInterface/Controllers/Setting/Mobile Number/MobileNumberIntroductionViewController.swift +++ b/Mixin/UserInterface/Controllers/Setting/Mobile Number/MobileNumberIntroductionViewController.swift @@ -1,4 +1,5 @@ import UIKit +import MixinServices final class MobileNumberIntroductionViewController: IntroductionViewController { @@ -44,14 +45,22 @@ final class MobileNumberIntroductionViewController: IntroductionViewController { actionButton.setTitle(R.string.localizable.continue(), for: .normal) actionButton.titleLabel?.setFont(scaledFor: .systemFont(ofSize: 16, weight: .medium), adjustForContentSize: true) actionButton.addTarget(self, action: #selector(continueToNext(_:)), for: .touchUpInside) + + if let source = UserOperationAnalytics.addMobileNumberSource?.rawValue { + reporter.report(event: .addPhoneStart, tags: ["source": source]) + } else { + reporter.report(event: .addPhoneStart) + } } @objc private func continueToNext(_ sender: Any) { - let next = switch action { + let next: UIViewController + switch action { case .add: - VerifyMobileNumberPINValidationViewController(intent: .addMobileNumber) + reporter.report(event: .addPhoneVerifyPIN) + next = VerifyMobileNumberPINValidationViewController(intent: .addMobileNumber) case .change: - VerifyMobileNumberPINValidationViewController(intent: .changeMobileNumber) + next = VerifyMobileNumberPINValidationViewController(intent: .changeMobileNumber) } navigationController?.pushViewController(replacingCurrent: next, animated: true) } diff --git a/Mixin/UserInterface/Controllers/Setting/Mobile Number/VerifyMobileNumberOneTimeCodeViewController.swift b/Mixin/UserInterface/Controllers/Setting/Mobile Number/VerifyMobileNumberOneTimeCodeViewController.swift index 13abfce0dc..e627efbcbb 100644 --- a/Mixin/UserInterface/Controllers/Setting/Mobile Number/VerifyMobileNumberOneTimeCodeViewController.swift +++ b/Mixin/UserInterface/Controllers/Setting/Mobile Number/VerifyMobileNumberOneTimeCodeViewController.swift @@ -70,21 +70,23 @@ class VerifyMobileNumberOneTimeCodeViewController: VerificationCodeViewControlle case .success(let account): LoginManager.shared.setAccount(account) self.verificationCodeField.resignFirstResponder() - let alert = switch self.context.intent { + let alert: UIAlertController + switch self.context.intent { case .periodicVerification: - UIAlertController( + alert = UIAlertController( title: R.string.localizable.verification_successful(), message: R.string.localizable.sms_verified_description(), preferredStyle: .alert ) case .addMobileNumber: - UIAlertController( + alert = UIAlertController( title: R.string.localizable.mobile_number_added(), message: R.string.localizable.mobile_number_added_description(), preferredStyle: .alert ) + reporter.report(event: .addPhoneEnd) case .changeMobileNumber: - UIAlertController( + alert = UIAlertController( title: R.string.localizable.mobile_number_changed(), message: R.string.localizable.sms_verified_description(), preferredStyle: .alert diff --git a/Mixin/UserInterface/Controllers/Setting/RecoveryKitViewController.swift b/Mixin/UserInterface/Controllers/Setting/RecoveryKitViewController.swift index 0b6367a0e1..ba57202d7d 100644 --- a/Mixin/UserInterface/Controllers/Setting/RecoveryKitViewController.swift +++ b/Mixin/UserInterface/Controllers/Setting/RecoveryKitViewController.swift @@ -87,15 +87,17 @@ extension RecoveryKitViewController: UITableViewDelegate { let next: UIViewController switch indexPath.row { case 0: - next = if let number = account.phone, !account.isAnonymous { - ChangeMobileNumberViewController(phoneNumber: number) + if let number = account.phone, !account.isAnonymous { + next = ChangeMobileNumberViewController(phoneNumber: number) } else { - AddMobileNumberViewController() + UserOperationAnalytics.addMobileNumberSource = .recoveryKitGuide + next = AddMobileNumberViewController() } case 1: next = ExportMnemonicPhrasesViewController() default: if account.isAnonymous { + UserOperationAnalytics.addMobileNumberSource = .recoveryKitGuide let tip = PopupTipViewController(tip: .addMobileNumber(.setRecoveryContact)) present(tip, animated: true) return diff --git a/Mixin/UserInterface/Controllers/Wallet/Buy/Account+BuyToken.swift b/Mixin/UserInterface/Controllers/Wallet/Buy/Account+BuyToken.swift index be7a0c8955..af2d14f474 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Buy/Account+BuyToken.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Buy/Account+BuyToken.swift @@ -18,6 +18,7 @@ extension Account { onVerificationNeeded: (PopupTipViewController) -> Void, ) { if isAnonymous { + UserOperationAnalytics.addMobileNumberSource = .buyGuide let tip = PopupTipViewController(tip: .addMobileNumber(.buyToken)) onVerificationNeeded(tip) } else if isPhoneVerificationValid { diff --git a/Mixin/UserInterface/Controllers/Wallet/CommonWalletViewController.swift b/Mixin/UserInterface/Controllers/Wallet/CommonWalletViewController.swift index 2ed086a0ce..ed9c998d5a 100644 --- a/Mixin/UserInterface/Controllers/Wallet/CommonWalletViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/CommonWalletViewController.swift @@ -465,6 +465,7 @@ extension CommonWalletViewController: WalletHeaderView.Delegate { self?.present(selector, animated: true, completion: nil) } case .trade: + UserOperationAnalytics.tradeSource = .walletHome let trade = TradeViewController( wallet: .common(wallet), supportedChainIDs: supportedChainIDs, @@ -477,14 +478,7 @@ extension CommonWalletViewController: WalletHeaderView.Delegate { return } withAccountRecoveryChecked { [weak self] in - guard let self else { - return - } - self.navigationController?.pushViewController(trade, animated: true) - reporter.report( - event: .tradeStart, - tags: ["wallet": "web3", "source": "wallet_home"] - ) + self?.navigationController?.pushViewController(trade, animated: true) } } } diff --git a/Mixin/UserInterface/Controllers/Wallet/Market/MarketViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Market/MarketViewController.swift index 131f56d9d5..1b4232533d 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Market/MarketViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Market/MarketViewController.swift @@ -662,6 +662,7 @@ extension MarketViewController: PillActionView.Delegate { alert(R.string.localizable.swap_not_supported(market.symbol)) } else { pickSingleToken { token in + UserOperationAnalytics.tradeSource = .marketDetail let trade = TradeViewController( wallet: .privacy, trading: nil, @@ -671,10 +672,6 @@ extension MarketViewController: PillActionView.Delegate { ) if let trade { self.navigationController?.pushViewController(trade, animated: true) - reporter.report( - event: .tradeStart, - tags: ["wallet": "main", "source": "market_detail"] - ) } } } diff --git a/Mixin/UserInterface/Controllers/Wallet/MixinTokenViewController.swift b/Mixin/UserInterface/Controllers/Wallet/MixinTokenViewController.swift index 8e82ba0d94..441d3a8970 100644 --- a/Mixin/UserInterface/Controllers/Wallet/MixinTokenViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/MixinTokenViewController.swift @@ -192,6 +192,7 @@ extension MixinTokenViewController: TokenActionView.Delegate { self?.send() } case .trade: + UserOperationAnalytics.tradeSource = .assetDetail let trade = TradeViewController( wallet: .privacy, trading: nil, @@ -203,14 +204,7 @@ extension MixinTokenViewController: TokenActionView.Delegate { return } withAccountRecoveryChecked { [weak self] in - guard let self else { - return - } - self.navigationController?.pushViewController(trade, animated: true) - reporter.report( - event: .tradeStart, - tags: ["wallet": "main", "source": "asset_detail"] - ) + self?.navigationController?.pushViewController(trade, animated: true) } case .buy: break diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift index 7a96804302..3ebbc10ce8 100644 --- a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPActionViewController.swift @@ -135,7 +135,6 @@ final class TIPActionViewController: UIViewController { try await TIP.registerDefaultCommonWalletIfNeeded(pin: pin) AppGroupUserDefaults.User.loginPINValidated = true await MainActor.run { - reporter.report(event: .signUpEnd) finish() } } catch { diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift index 7c793fa688..c21c542a26 100644 --- a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPIntroViewController.swift @@ -156,14 +156,6 @@ final class TIPIntroViewController: UIViewController { case .inputNeeded(let context): let navigationController = self.tipNavigationController let validator = TIPPopupInputViewController(action: .continue(context, { [weak navigationController] in - switch context.action { - case .create: - reporter.report(event: .signUpEnd) - case .change: - break - case .migrate: - break - } navigationController?.finish() })) present(validator, animated: true) diff --git a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPNavigationController.swift b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPNavigationController.swift index 62fa187105..8b0a814b41 100644 --- a/Mixin/UserInterface/Controllers/Wallet/PIN/TIPNavigationController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/PIN/TIPNavigationController.swift @@ -3,8 +3,6 @@ import MixinServices final class TIPNavigationController: GeneralAppearanceNavigationController { - var redirectsToWalletTabOnFinished = false - convenience init(intent: TIP.Action) { Logger.tip.info(category: "TIPNavigation", message: "Init with intent: \(intent)") let intro = TIPIntroViewController(intent: intent) @@ -32,12 +30,16 @@ final class TIPNavigationController: GeneralAppearanceNavigationController { func finish() { if AppDelegate.current.mainWindow.rootViewController == self { + if AppGroupUserDefaults.isSigningUp { + reporter.report(event: .signUpEnd) + } else { + reporter.report(event: .loginEnd) + } Logger.tip.info(category: "TIPNavigation", message: "Finished") Logger.redirectLogsToLogin = false AppDelegate.current.mainWindow.rootViewController = HomeContainerViewController( - initialTab: redirectsToWalletTabOnFinished ? .wallet : .chat + initialTab: .wallet ) - reporter.report(event: .loginEnd) } else { presentingViewController?.dismiss(animated: true) } diff --git a/Mixin/UserInterface/Controllers/Wallet/Payment/OpenPerpetualPositionPreviewViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Payment/OpenPerpetualPositionPreviewViewController.swift index 22948285c7..54c41778c1 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Payment/OpenPerpetualPositionPreviewViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Payment/OpenPerpetualPositionPreviewViewController.swift @@ -185,6 +185,16 @@ final class OpenPerpetualPositionPreviewViewController: WalletIdentifyingAuthent reloadData(with: rows) } + override func confirm(_ sender: Any) { + super.confirm(sender) + reporter.report(event: .tradePerpsPreviewConfirm) + } + + override func close(_ sender: Any) { + super.close(sender) + reporter.report(event: .tradePerpsPreviewCancel) + } + override func performAction(with pin: String) { canDismissInteractively = false tableHeaderView.setIcon(progress: .busy) @@ -223,6 +233,13 @@ final class OpenPerpetualPositionPreviewViewController: WalletIdentifyingAuthent if let callback = context.onDismissAfterSuccess { onDismiss = callback } + reporter.report( + event: .tradePerpsOpenPositionEnd, + tags: [ + "leverage": "\(context.leverageMultiplier)", + "trade_asset_level": operation.amount.reportingAssetLevel, + ] + ) } } catch { let errorDescription = if let error = error as? MixinAPIError, PINVerificationFailureHandler.canHandle(error: error) { diff --git a/Mixin/UserInterface/Controllers/Wallet/Payment/TradeSpotPreviewViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Payment/TradeSpotPreviewViewController.swift index 0cf0546e09..511a4543d0 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Payment/TradeSpotPreviewViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Payment/TradeSpotPreviewViewController.swift @@ -198,19 +198,19 @@ final class TradeSpotPreviewViewController: WalletIdentifyingAuthenticationPrevi subtitle: R.string.localizable.signature_request_from(.mixin) ) replaceTrayView(with: nil, animation: .vertical) - let reportType = switch mode { - case .simple: - "swap" - case .advanced: - "limit" - } let assetIDs = [sendToken.assetID, receiveToken.assetID] Task { do { switch operation { case .mixin(let operation): try await operation.start(pin: pin) - reporter.report(event: .tradeEnd, tags: ["wallet": "main", "type": reportType, "trade_asset_level": sendAmount.reportingAssetLevel]) + reporter.report( + event: .tradeSpotEnd, + tags: [ + "wallet": "main", + "trade_asset_level": sendAmount.reportingAssetLevel + ] + ) let inexistAssetIDs = TokenDAO.shared.inexistAssetIDs(in: assetIDs) for assetID in inexistAssetIDs { let job = RefreshTokenJob(assetID: assetID) @@ -218,7 +218,13 @@ final class TradeSpotPreviewViewController: WalletIdentifyingAuthenticationPrevi } case .web3(let operation): try await operation.start(pin: pin) - reporter.report(event: .tradeEnd, tags: ["wallet": "web3", "type": reportType, "trade_asset_level": sendAmount.reportingAssetLevel]) + reporter.report( + event: .tradeSpotEnd, + tags: [ + "wallet": "web3", + "trade_asset_level": sendAmount.reportingAssetLevel + ] + ) let walletID = operation.wallet.walletID let inexistAssetIDs = Web3TokenDAO.shared.inexistAssetIDs(walletID: walletID, in: assetIDs) for assetID in inexistAssetIDs { diff --git a/Mixin/UserInterface/Controllers/Wallet/PrivacyWalletViewController.swift b/Mixin/UserInterface/Controllers/Wallet/PrivacyWalletViewController.swift index fc08c1f136..43f1611bc2 100644 --- a/Mixin/UserInterface/Controllers/Wallet/PrivacyWalletViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/PrivacyWalletViewController.swift @@ -330,6 +330,7 @@ extension PrivacyWalletViewController: WalletHeaderView.Delegate { self?.present(selector, animated: true, completion: nil) } case .trade: + UserOperationAnalytics.tradeSource = .walletHome let trade = TradeViewController( wallet: .privacy, trading: nil, @@ -347,10 +348,6 @@ extension PrivacyWalletViewController: WalletHeaderView.Delegate { self.navigationController?.pushViewController(trade, animated: true) self.tableHeaderView.actionView.badgeActions.remove(.trade) BadgeManager.shared.setHasViewed(identifier: .trade) - reporter.report( - event: .tradeStart, - tags: ["wallet": "main", "source": "wallet_home"] - ) } } } diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/AddPerpsPositionViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/AddPerpsPositionViewController.swift index a6f1f2f983..312a35005e 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/AddPerpsPositionViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/AddPerpsPositionViewController.swift @@ -169,11 +169,13 @@ final class AddPerpsPositionViewController: PerpsMarginInputViewController { @IBAction func introduceSize(_ sender: Any) { let manual = PerpsManual.viewController(initialPage: .size) present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_add_position_size"]) } @IBAction func introduceLiquidationPrice(_ sender: Any) { let manual = PerpsManual.viewController(initialPage: .liquidation) present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_add_position_liquidation"]) } @IBAction func cancel(_ sender: Any) { diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/ClosePerpetualPositionPreviewViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/ClosePerpetualPositionPreviewViewController.swift index a673339ea9..0253005277 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/ClosePerpetualPositionPreviewViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/ClosePerpetualPositionPreviewViewController.swift @@ -67,6 +67,16 @@ final class ClosePerpetualPositionPreviewViewController: WalletIdentifyingAuthen reloadData(with: rows) } + override func confirm(_ sender: Any) { + super.confirm(sender) + reporter.report(event: .tradePerpsClosePositionPreviewConfirm) + } + + override func close(_ sender: Any) { + super.close(sender) + reporter.report(event: .tradePerpsClosePositionPreviewCancel) + } + override func performAction(with pin: String) { canDismissInteractively = false tableHeaderView.setIcon(progress: .busy) @@ -76,7 +86,6 @@ final class ClosePerpetualPositionPreviewViewController: WalletIdentifyingAuthen ) replaceTrayView(with: nil, animation: .vertical) let positionID = viewModel.positionID - let walletID = viewModel.wallet.tradingWalletID Task { do { try await AccountAPI.verify(pin: pin) @@ -91,6 +100,7 @@ final class ClosePerpetualPositionPreviewViewController: WalletIdentifyingAuthen ) tableView.setContentOffset(.zero, animated: true) loadFinishedTrayView() + reporter.report(event: .tradePerpsClosePositionEnd) } } catch { let errorDescription = if let error = error as? MixinAPIError, PINVerificationFailureHandler.canHandle(error: error) { diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/OpenPerpetualPositionViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/OpenPerpetualPositionViewController.swift index f2ef7bd6da..257bdf2993 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/OpenPerpetualPositionViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/OpenPerpetualPositionViewController.swift @@ -209,6 +209,10 @@ final class OpenPerpetualPositionViewController: PerpsMarginInputViewController leverageMultiplier: leverageMultiplier, underlyingAsset: viewModel ) + + var tags = ["direction": side.rawValue] + tags["source"] = UserOperationAnalytics.tradeSource?.rawValue + reporter.report(event: .tradePerpsOpenPositionStart, tags: tags) } override func editMarginAmount(_ textField: UITextField) { @@ -272,17 +276,20 @@ final class OpenPerpetualPositionViewController: PerpsMarginInputViewController @IBAction func presentAutoClosingManual(_ sender: Any) { let manual = PerpsManual.viewController(initialPage: .autoClosing) present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_open_position_auto_closing"]) } @IBAction func presentOrderValueManual(_ sender: Any) { let manual = PerpsManual.viewController(initialPage: .size) present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_open_position_size"]) } @IBAction func review(_ sender: ConfigurationBasedBusyButton) { guard let marginToken, let liquidationPrice else { return } + reporter.report(event: .tradePerpsPreview) marginAmountTextField.resignFirstResponder() let request = OpenPerpetualOrderRequest( assetID: marginToken.assetID, @@ -346,7 +353,7 @@ final class OpenPerpetualPositionViewController: PerpsMarginInputViewController @objc private func presentCustomerService(_ sender: Any) { let customerService = CustomerServiceViewController() present(customerService, animated: true) - reporter.report(event: .customerServiceDialog, tags: ["source": "open_perps_position"]) + reporter.report(event: .customerServiceDialog, tags: ["source": "perps_open_position"]) } private func inputCustomLeverageMultiplier() { @@ -517,6 +524,7 @@ extension OpenPerpetualPositionViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { switch multipliers[indexPath.item] { case .custom: + reporter.report(event: .tradePerpsLeverageSelect, tags: ["leverage": "custom_tab"]) inputCustomLeverageMultiplier() return false default: @@ -527,6 +535,7 @@ extension OpenPerpetualPositionViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { switch multipliers[indexPath.item] { case .custom: + reporter.report(event: .tradePerpsLeverageSelect, tags: ["leverage": "custom_tab"]) inputCustomLeverageMultiplier() default: break @@ -538,8 +547,10 @@ extension OpenPerpetualPositionViewController: UICollectionViewDelegate { marginAmountTextField.resignFirstResponder() switch multipliers[indexPath.item] { case .fixed(let leverage): + reporter.report(event: .tradePerpsLeverageSelect, tags: ["leverage": "\(leverage)x"]) inputLeverageMultiplier(value: leverage) case .max: + reporter.report(event: .tradePerpsLeverageSelect, tags: ["leverage": "max"]) inputLeverageMultiplier(value: viewModel.maxLeverageMultiplier) case .custom: break @@ -551,6 +562,7 @@ extension OpenPerpetualPositionViewController: UICollectionViewDelegate { extension OpenPerpetualPositionViewController: UITextFieldDelegate { func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + reporter.report(event: .tradePerpsLeverageSelect, tags: ["leverage": "custom_input"]) inputCustomLeverageMultiplier() return false } diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualActivitiesViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualActivitiesViewController.swift index e071081e82..0cd2455bb9 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualActivitiesViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualActivitiesViewController.swift @@ -177,6 +177,7 @@ extension PerpetualActivitiesViewController: UICollectionViewDataSource { cell.onHelp = { [weak self] in let manual = PerpsManual.viewController() self?.present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_activities"]) } return cell } @@ -202,6 +203,7 @@ extension PerpetualActivitiesViewController: UICollectionViewDelegate { let viewModel = viewModels[indexPath.item] let activity = PerpetualActivityViewController(wallet: wallet, viewModel: viewModel) navigationController?.pushViewController(activity, animated: true) + reporter.report(event: .tradePerpsActivityDetail, tags: ["source": "perps_activity_list"]) } } diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualMarketViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualMarketViewController.swift index 4cf94ac190..3ebd503915 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualMarketViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualMarketViewController.swift @@ -253,6 +253,7 @@ final class PerpetualMarketViewController: UIViewController { BadgeManager.shared.setHasViewed(identifier: .perpsManual) let manual = PerpsManual.viewController() present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "first_guide"]) } } @@ -309,6 +310,7 @@ final class PerpetualMarketViewController: UIViewController { } let preview = ClosePerpetualPositionPreviewViewController(viewModel: viewModel) present(preview, animated: true) + reporter.report(event: .tradePerpsClosePositionStart) } @objc private func reloadMarket(_ notification: Notification) { @@ -420,6 +422,10 @@ final class PerpetualMarketViewController: UIViewController { private func viewActivities() { let activities = PerpetualActivitiesViewController(wallet: wallet) navigationController?.pushViewController(activities, animated: true) + reporter.report( + event: .tradePerpsActivity, + tags: ["source": "perps_market"] + ) } private func handleTPSLUpdate(result: MixinAPI.Result) { @@ -771,9 +777,11 @@ extension PerpetualMarketViewController: UICollectionViewDelegate { let viewModel = viewModels[indexPath.item] let activity = PerpetualActivityViewController(wallet: wallet, viewModel: viewModel) navigationController?.pushViewController(activity, animated: true) + reporter.report(event: .tradePerpsActivityDetail, tags: ["source": "perps_market"]) case .introduction: let manual = PerpsManual.viewController() present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_detail_card"]) } } @@ -797,6 +805,7 @@ extension PerpetualMarketViewController: PerpetualMarketOpenPositionCell.Delegat func perpetualMarketOpenPositionCell(_ cell: PerpetualMarketOpenPositionCell, requestManual page: PerpsManual.Page) { let manual = PerpsManual.viewController(initialPage: page) present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_detail_card"]) } func perpetualMarketOpenPositionCellAskToShare(_ cell: PerpetualMarketOpenPositionCell) { diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualPositionsViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualPositionsViewController.swift index 7eb9179a29..f923e93597 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualPositionsViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpetualPositionsViewController.swift @@ -188,6 +188,7 @@ extension PerpetualPositionsViewController: UICollectionViewDataSource { cell.onHelp = { [weak self] in let manual = PerpsManual.viewController() self?.present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_all_positions"]) } return cell } diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpsMarginInputViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpsMarginInputViewController.swift index 0897d6a02e..1678416b31 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpsMarginInputViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/PerpsMarginInputViewController.swift @@ -51,12 +51,15 @@ class PerpsMarginInputViewController: UIViewController { accessoryView.items = [ .init(title: "25%") { [weak self] in self?.inputAmount(withBalanceMultipliedBy: 0.25) + reporter.report(event: .tradePerpsAmountInputPercent, tags: ["percent": "25%"]) }, .init(title: "50%") { [weak self] in self?.inputAmount(withBalanceMultipliedBy: 0.5) + reporter.report(event: .tradePerpsAmountInputPercent, tags: ["percent": "50%"]) }, .init(title: R.string.localizable.max()) { [weak self] in self?.inputAmount(withBalanceMultipliedBy: 1) + reporter.report(event: .tradePerpsAmountInputPercent, tags: ["percent": "max"]) }, ] accessoryView.onDone = { [weak textField=marginAmountTextField] in @@ -112,6 +115,7 @@ class PerpsMarginInputViewController: UIViewController { @IBAction func inputTokenBalance(_ sender: Any) { inputAmount(withBalanceMultipliedBy: 1) + reporter.report(event: .tradePerpsAmountInputBalance) } @IBAction func depositMarginToken(_ sender: Any) { @@ -243,6 +247,7 @@ extension PerpsMarginInputViewController: AddTokenMethodSelectorViewController.D let count = viewControllers.count - index viewControllers.removeLast(count) } + UserOperationAnalytics.tradeSource = .perpsMarginInput let trade = TradeViewController( wallet: .privacy, trading: .simpleSpot, diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/TradePerpetualViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/TradePerpetualViewController.swift index fb3785cb53..fce0655e71 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/TradePerpetualViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Perpetual/TradePerpetualViewController.swift @@ -224,6 +224,7 @@ final class TradePerpetualViewController: UIViewController { BadgeManager.shared.setHasViewed(identifier: .perpsManual) let manual = PerpsManual.viewController() present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "first_guide"]) } } @@ -336,6 +337,7 @@ extension TradePerpetualViewController: UICollectionViewDataSource { cell.emptyIndicatorStackView.isHidden = false cell.onHelp = { [weak self] in self?.presentPerpsManual() + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_home_card"]) } } return cell @@ -357,6 +359,7 @@ extension TradePerpetualViewController: UICollectionViewDataSource { cell.emptyIndicatorStackView.isHidden = false cell.onHelp = { [weak self] in self?.presentPerpsManual() + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_home_card"]) } } return cell @@ -394,6 +397,10 @@ extension TradePerpetualViewController: UICollectionViewDataSource { view.label.text = R.string.localizable.perps_activity() view.onShowAll = { [weak self] (sender) in self?.viewActivities() + reporter.report( + event: .tradePerpsActivity, + tags: ["source": "perps_home_card_arrow"] + ) } } return view @@ -416,6 +423,10 @@ extension TradePerpetualViewController: UICollectionViewDataSource { view.viewAllButton.isHidden = (activities?.count ?? 0) <= maxItemCount view.onViewAll = { [weak self] (sender) in self?.viewActivities() + reporter.report( + event: .tradePerpsActivity, + tags: ["source": "perps_home_card_more"] + ) } } return view @@ -460,8 +471,10 @@ extension TradePerpetualViewController: UICollectionViewDelegate { let viewModel = activities[indexPath.item] let activity = PerpetualActivityViewController(wallet: wallet, viewModel: viewModel) navigationController?.pushViewController(activity, animated: true) + reporter.report(event: .tradePerpsActivityDetail, tags: ["source": "perps_home_list"]) case .introduction: presentPerpsManual() + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_home_card"]) } } diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeMixinSpotViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeMixinSpotViewController.swift index e8832f9dba..459de9b532 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeMixinSpotViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeMixinSpotViewController.swift @@ -38,6 +38,13 @@ final class TradeMixinSpotViewController: TradeSpotViewController { fatalError("Storyboard not supported") } + override func viewDidLoad() { + super.viewDidLoad() + var tags = ["wallet": "main", "type": mode.rawValue] + tags["source"] = UserOperationAnalytics.tradeSource?.rawValue + reporter.report(event: .tradeSpotStart, tags: tags) + } + override func changeSendToken(_ sender: Any) { let selector = TradeMixinTokenSelectorViewController( intent: .send, diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeOrderViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeOrderViewController.swift index c569d820b9..4ceac68f9f 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeOrderViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeOrderViewController.swift @@ -260,6 +260,7 @@ extension TradeOrderViewController: PillActionView.Delegate { case .swap, .none: trading = .simpleSpot } + UserOperationAnalytics.tradeSource = .tradeDetail let trade = TradeViewController( wallet: viewModel.wallet, trading: trading, @@ -270,10 +271,6 @@ extension TradeOrderViewController: PillActionView.Delegate { if let trade { viewControllers.append(trade) navigationController.setViewControllers(viewControllers, animated: true) - reporter.report( - event: .tradeStart, - tags: ["wallet": "main", "source": "trade_detail"] - ) } } case .cancelOrder: diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeSpotViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeSpotViewController.swift index 558ccc42c2..02e1efd615 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeSpotViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeSpotViewController.swift @@ -4,7 +4,7 @@ import MixinServices class TradeSpotViewController: UIViewController { - enum Mode { + enum Mode: String { case simple case advanced } diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeWeb3SpotViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeWeb3SpotViewController.swift index 9f85cc3e1e..33262ecca9 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeWeb3SpotViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/Spot/TradeWeb3SpotViewController.swift @@ -44,6 +44,13 @@ final class TradeWeb3SpotViewController: TradeSpotViewController { fatalError("Storyboard not supported") } + override func viewDidLoad() { + super.viewDidLoad() + var tags = ["wallet": "web3", "type": mode.rawValue] + tags["source"] = UserOperationAnalytics.tradeSource?.rawValue + reporter.report(event: .tradeSpotStart, tags: tags) + } + override func changeSendToken(_ sender: Any) { let selector = TradeWeb3TokenSelectorViewController( wallet: wallet, diff --git a/Mixin/UserInterface/Controllers/Wallet/Trade/TradeViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Trade/TradeViewController.swift index 2e1b4bc456..5f9864471d 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Trade/TradeViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Trade/TradeViewController.swift @@ -191,11 +191,19 @@ final class TradeViewController: UIViewController { } @objc func presentCustomerService(_ sender: Any) { + let trading = self.trading let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) sheet.addAction(UIAlertAction(title: R.string.localizable.contact_support(), style: .default, handler: { _ in let customerService = CustomerServiceViewController() self.present(customerService, animated: true) - reporter.report(event: .customerServiceDialog, tags: ["source": "trade_home"]) + switch trading { + case .simpleSpot: + reporter.report(event: .customerServiceDialog, tags: ["source": "trade_simple_home_menu"]) + case .advancedSpot: + reporter.report(event: .customerServiceDialog, tags: ["source": "trade_advanced_home_menu"]) + case .perpetualFutures: + reporter.report(event: .customerServiceDialog, tags: ["source": "perps_home_menu"]) + } })) switch trading { case .simpleSpot, .advancedSpot: @@ -207,6 +215,7 @@ final class TradeViewController: UIViewController { sheet.addAction(UIAlertAction(title: R.string.localizable.perpetual_futures_guide(), style: .default, handler: { _ in let manual = PerpsManual.viewController() self.present(manual, animated: true) + reporter.report(event: .tradePerpsGuide, tags: ["source": "perps_home_menu"]) })) } sheet.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .cancel, handler: nil)) diff --git a/Mixin/UserInterface/Controllers/Wallet/Transfer/TransferInputAmountViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Transfer/TransferInputAmountViewController.swift index 79a5921030..cf2107cf66 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Transfer/TransferInputAmountViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Transfer/TransferInputAmountViewController.swift @@ -260,9 +260,11 @@ extension TransferInputAmountViewController: AddTokenMethodSelectorViewControlle _ viewController: AddTokenMethodSelectorViewController, didPickMethod method: AddTokenMethodSelectorViewController.Method ) { - let next = switch method { + let next: UIViewController? + switch method { case .trade: - TradeViewController( + UserOperationAnalytics.tradeSource = .transfer + next = TradeViewController( wallet: .privacy, trading: .simpleSpot, sendAssetID: nil, @@ -270,7 +272,7 @@ extension TransferInputAmountViewController: AddTokenMethodSelectorViewControlle referral: nil ) case .deposit: - DepositViewController(token: tokenItem, switchingBetweenNetworks: false) + next = DepositViewController(token: tokenItem, switchingBetweenNetworks: false) } if let next { navigationController?.pushViewController(next, animated: true) diff --git a/Mixin/UserInterface/Controllers/Wallet/Transfer/WithdrawInputAmountViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Transfer/WithdrawInputAmountViewController.swift index bc255bcec8..310ac71acf 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Transfer/WithdrawInputAmountViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Transfer/WithdrawInputAmountViewController.swift @@ -288,9 +288,11 @@ extension WithdrawInputAmountViewController: AddTokenMethodSelectorViewControlle guard let feeToken = selectedFeeItem?.token else { return } - let next = switch method { + let next: UIViewController? + switch method { case .trade: - TradeViewController( + UserOperationAnalytics.tradeSource = .withdraw + next = TradeViewController( wallet: .privacy, trading: .simpleSpot, sendAssetID: nil, @@ -298,7 +300,7 @@ extension WithdrawInputAmountViewController: AddTokenMethodSelectorViewControlle referral: nil ) case .deposit: - DepositViewController(token: feeToken, switchingBetweenNetworks: false) + next = DepositViewController(token: feeToken, switchingBetweenNetworks: false) } if let next { navigationController?.pushViewController(next, animated: true) diff --git a/Mixin/UserInterface/Controllers/Wallet/Web3TokenViewController.swift b/Mixin/UserInterface/Controllers/Wallet/Web3TokenViewController.swift index 6c93f5cad2..2a372c2ab1 100644 --- a/Mixin/UserInterface/Controllers/Wallet/Web3TokenViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/Web3TokenViewController.swift @@ -235,6 +235,7 @@ extension Web3TokenViewController: TokenActionView.Delegate { self?.send() } case .trade: + UserOperationAnalytics.tradeSource = .assetDetail let trade = TradeViewController( wallet: .common(wallet), trading: nil, @@ -246,14 +247,7 @@ extension Web3TokenViewController: TokenActionView.Delegate { return } withAccountRecoveryChecked { [weak self] in - guard let self else { - return - } - self.navigationController?.pushViewController(trade, animated: true) - reporter.report( - event: .tradeStart, - tags: ["wallet": "web3", "source": "asset_detail"] - ) + self?.navigationController?.pushViewController(trade, animated: true) } case .buy: break diff --git a/Mixin/UserInterface/Controllers/Web3/Web3TransferInputAmountViewController.swift b/Mixin/UserInterface/Controllers/Web3/Web3TransferInputAmountViewController.swift index d5b95bedd8..7b0dc1b0a2 100644 --- a/Mixin/UserInterface/Controllers/Web3/Web3TransferInputAmountViewController.swift +++ b/Mixin/UserInterface/Controllers/Web3/Web3TransferInputAmountViewController.swift @@ -292,9 +292,11 @@ extension Web3TransferInputAmountViewController: AddTokenMethodSelectorViewContr guard let feeToken = fee?.selected.token else { return } - let next = switch method { + let next: UIViewController? + switch method { case .trade: - TradeViewController( + UserOperationAnalytics.tradeSource = .transfer + next = TradeViewController( wallet: .common(payment.wallet), trading: .simpleSpot, sendAssetID: nil, @@ -302,7 +304,7 @@ extension Web3TransferInputAmountViewController: AddTokenMethodSelectorViewContr referral: nil ) case .deposit: - DepositViewController( + next = DepositViewController( wallet: payment.wallet, token: feeToken, switchingBetweenNetworks: false diff --git a/Mixin/UserInterface/Windows/UrlWindow.swift b/Mixin/UserInterface/Windows/UrlWindow.swift index 3cf95ab1de..1524e0ebcb 100644 --- a/Mixin/UserInterface/Windows/UrlWindow.swift +++ b/Mixin/UserInterface/Windows/UrlWindow.swift @@ -86,10 +86,6 @@ class UrlWindow { viewModel: viewModel, ) navigationController.pushViewController(market, animated: true) - reporter.report( - event: .tradeStart, - tags: ["wallet": "main", "source": "schema"] - ) } case .failure(let error): hud.set(style: .error, text: error.localizedDescription) @@ -104,6 +100,7 @@ class UrlWindow { let mode = AppGroupUserDefaults.Wallet.tradeMode trading = TradeViewController.Trading(rawValue: mode) ?? .simpleSpot } + UserOperationAnalytics.tradeSource = .scheme let trade = TradeViewController( wallet: .privacy, trading: trading, @@ -113,10 +110,6 @@ class UrlWindow { ) if let navigationController = UIApplication.homeNavigationController, let trade { navigationController.pushViewController(trade, animated: true) - reporter.report( - event: .tradeStart, - tags: ["wallet": "main", "source": "schema"] - ) } } return true diff --git a/MixinServices/MixinServices/Foundation/Reporter.swift b/MixinServices/MixinServices/Foundation/Reporter.swift index 78b32cab92..ac8a44bb91 100644 --- a/MixinServices/MixinServices/Foundation/Reporter.swift +++ b/MixinServices/MixinServices/Foundation/Reporter.swift @@ -4,13 +4,14 @@ import Bugsnag open class Reporter { public enum Event: String { - case signUpStart = "sign_up_start" - case signUpCAPTCHA = "sign_up_captcha" - case signUpFullname = "sign_up_fullname" - case signUpSignalInit = "sign_up_signal_init" - case signUpPINSet = "sign_up_pin_set" - case signUpPINQuiz = "sign_up_pin_quiz" - case signUpEnd = "sign_up_end" + case signUpStart = "sign_up_start" + case signUpCAPTCHA = "sign_up_captcha" + case signUpAccountCreated = "sign_up_account_created" + case signUpFullname = "sign_up_fullname" + case signUpSignalInit = "sign_up_signal_init" + case signUpPINSet = "sign_up_pin_set" + case signUpPINQuiz = "sign_up_pin_quiz" + case signUpEnd = "sign_up_end" case loginStart = "login_start" case loginSMSSendConfirmed = "login_sms_send_confirmed" @@ -22,13 +23,33 @@ open class Reporter { case loginVerifyPIN = "login_pin_verify" case loginEnd = "login_end" - case tradeStart = "trade_start" + case addPhoneStart = "add_phone_start" + case addPhoneVerifyPIN = "add_phone_verify_pin" + case addPhoneEnd = "add_phone_end" + + case tradeSpotStart = "trade_spot_start" + case tradeSpotEnd = "trade_spot_end" case tradeTokenSelect = "trade_token_select" case tradeQuote = "trade_quote" case tradePreview = "trade_preview" - case tradeEnd = "trade_end" case tradeTransactions = "trade_transactions" case tradeDetail = "trade_detail" + case tradePerpsOpenPositionStart = "trade_perps_open_position_start" + case tradePerpsMarginTokenSelect = "trade_perps_margin_token_select" + case tradePerpsAmountInputPercent = "trade_perps_amount_input_percent" + case tradePerpsAmountInputBalance = "trade_perps_amount_input_balance" + case tradePerpsLeverageSelect = "trade_perps_leverage_select" + case tradePerpsPreview = "trade_perps_preview" + case tradePerpsPreviewConfirm = "trade_perps_preview_confirm" + case tradePerpsPreviewCancel = "trade_perps_preview_cancel" + case tradePerpsOpenPositionEnd = "trade_perps_open_position_end" + case tradePerpsClosePositionStart = "trade_perps_close_position_start" + case tradePerpsClosePositionPreviewConfirm = "trade_perps_close_position_preview_confirm" + case tradePerpsClosePositionPreviewCancel = "trade_perps_close_position_preview_cancel" + case tradePerpsClosePositionEnd = "trade_perps_close_position_end" + case tradePerpsActivity = "trade_perps_activity" + case tradePerpsActivityDetail = "trade_perps_activity_detail" + case tradePerpsGuide = "trade_perps_guide" case buyStart = "buy_start" case buyTokenSelect = "buy_token_select" @@ -116,4 +137,8 @@ open class Reporter { } + open func updateUserProperty(key: String, value: String) { + + } + } diff --git a/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults.swift b/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults.swift index 9341782472..167e934e59 100644 --- a/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults.swift +++ b/MixinServices/MixinServices/Foundation/User Defaults/AppGroupUserDefaults.swift @@ -143,6 +143,9 @@ extension AppGroupUserDefaults { @Default(namespace: nil, key: "app_public_key", defaultValue: [:]) public static var appPublicKey: [String: Data] + @Default(namespace: nil, key: "signing_up", defaultValue: false) + public static var isSigningUp: Bool + } extension AppGroupUserDefaults {