Skip to content
4 changes: 4 additions & 0 deletions Mixin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -2893,6 +2894,7 @@
944F547E2E45F99F0064A9A2 /* WatchingAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchingAddresses.swift; sourceTree = "<group>"; };
94509BC02DDDFC5800DC6A43 /* RefreshWeb3TokenJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshWeb3TokenJob.swift; sourceTree = "<group>"; };
94509D762DE086D800DC6A43 /* IAPTransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPTransactionObserver.swift; sourceTree = "<group>"; };
9450EB632FC89647003534FF /* UserOperationAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserOperationAnalytics.swift; sourceTree = "<group>"; };
94510FBA2C85673C00ACD972 /* ReloadGlobalMarketJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadGlobalMarketJob.swift; sourceTree = "<group>"; };
94510FCD2C8742BC00ACD972 /* MarketTokenSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTokenSelectorViewController.swift; sourceTree = "<group>"; };
94510FD22C8743AA00ACD972 /* MarketTokenSelectorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTokenSelectorCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6639,6 +6641,7 @@
948CFAE92F2CB4B200BC34C3 /* QRCodeDetector.swift */,
94EC9CD32F2C7CC000B3CD82 /* VideoCaptureDevice.swift */,
9404D76F2FB1A72600F5563A /* SignPosition.swift */,
9450EB632FC89647003534FF /* UserOperationAnalytics.swift */,
);
path = Service;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
33 changes: 33 additions & 0 deletions Mixin/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)")
}

}
36 changes: 36 additions & 0 deletions Mixin/Service/MainAppReporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import UIKit
import FirebaseCore
import FirebaseAnalytics
import FirebaseCrashlytics
import AppsFlyerLib
import MixinServices

final class MainAppReporter: Reporter {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}

}
32 changes: 32 additions & 0 deletions Mixin/Service/UserOperationAnalytics.swift
Original file line number Diff line number Diff line change
@@ -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?

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
[
Expand Down Expand Up @@ -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)
Expand All @@ -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()
}
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand All @@ -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])
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading