Skip to content

Commit

Permalink
IOS-6610 IOS-7064 Add a mechanism for automatic indexation of wallets (
Browse files Browse the repository at this point in the history
…#3814)

Co-authored-by: Andrey Chukavin <[email protected]>
  • Loading branch information
amuraveinik and megakoko authored Sep 26, 2024
1 parent 438bae9 commit c00779b
Show file tree
Hide file tree
Showing 8 changed files with 541 additions and 3 deletions.
6 changes: 6 additions & 0 deletions Tangem/App/Models/UserWallet/StoredUserWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,9 @@ extension StoredUserWallet: Decodable {
}
}
}

protocol NameableWallet {
var name: String { get set }
}

extension StoredUserWallet: NameableWallet {}
3 changes: 3 additions & 0 deletions Tangem/App/Services/Common/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ class AppSettings {
@AppStorageCompat(StorageType.forcedDemoCardId)
var forcedDemoCardId: String? = nil

@AppStorageCompat(StorageType.didMigrateUserWalletNames)
var didMigrateUserWalletNames: Bool = false

static let shared: AppSettings = .init()

private init() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// UserWalletNameIndexationHelper.swift
// Tangem
//
// Created by Andrey Chukavin on 17.05.2024.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import Foundation

final class UserWalletNameIndexationHelper {
private var existingNames: Set<String>

private init(existingNames: [String]) {
self.existingNames = Set(existingNames).filter { NameComponents(from: $0) != nil }
}

static func migratedWallets<T: NameableWallet>(_ wallets: [T]) -> [T]? {
var wallets = wallets
let helper = UserWalletNameIndexationHelper(existingNames: wallets.map(\.name))

var didChangeNames = false
for (index, wallet) in wallets.enumerated() {
let suggestedName = helper.suggestedName(for: wallet.name)
if wallet.name != suggestedName {
var wallet = wallet
wallet.name = suggestedName
wallets[index] = wallet
didChangeNames = true
}
}

return didChangeNames ? wallets : nil
}

static func suggestedName(_ rawName: String, names: [String]) -> String {
if NameComponents(from: rawName) != nil {
return rawName
}

let indicesByNameTemplate = names.reduce(into: [String: Set<Int>]()) { dict, name in
guard let nameComponents = NameComponents(from: name) else {
dict[name, default: []].insert(1)
return
}

dict[nameComponents.template, default: []].insert(nameComponents.index)
}

let nameTemplate = rawName.trimmingCharacters(in: .whitespaces)
let nameIndex = indicesByNameTemplate.nextIndex(for: nameTemplate)

if nameIndex == 1 {
return nameTemplate
} else {
return "\(nameTemplate) \(nameIndex)"
}
}

private func suggestedName(for rawName: String) -> String {
let name = Self.suggestedName(rawName, names: Array(existingNames))
existingNames.insert(name)
return name
}
}

private extension UserWalletNameIndexationHelper {
struct NameComponents {
static let nameComponentsRegex = try! NSRegularExpression(pattern: "^(.+)(\\s+\\d+)$")

let template: String
let index: Int

init?(from rawName: String) {
let name = rawName.trimmingCharacters(in: .whitespaces)
let range = NSRange(location: 0, length: name.count)

guard
let match = Self.nameComponentsRegex.matches(in: name, range: range).first,
match.numberOfRanges == 3,
let templateRange = Range(match.range(at: 1), in: name),
let indexRange = Range(match.range(at: 2), in: name),
let index = Int(String(name[indexRange]).trimmingCharacters(in: .whitespaces))
else {
return nil
}

template = String(name[templateRange])
self.index = index
}
}
}

private extension [String: Set<Int>] {
func nextIndex(for nameTemplate: String) -> Int {
let indices = self[nameTemplate, default: []]

if indices.isEmpty {
return 1
}

for i in 1 ... indices.count {
if !indices.contains(i) {
return i
}
}

let defaultIndex = indices.count + 1
return defaultIndex
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,18 @@ class CommonUserWalletRepository: UserWalletRepository {
failedCardScanTracker.resetCounter()
sendEvent(.scan(isScanning: false))

let cardInfo = response.getCardInfo()
var cardInfo = response.getCardInfo()
updateAssociatedCard(for: cardInfo)
resetServices()
initializeAnalyticsContext(with: cardInfo)
let config = UserWalletConfigFactory(cardInfo).makeConfig()
Analytics.endLoggingCardScan()

cardInfo.name = UserWalletNameIndexationHelper.suggestedName(
config.cardName,
names: models.map(\.name)
)

let userWalletModel = CommonUserWalletModel(cardInfo: cardInfo)
if let userWalletModel {
initializeServices(for: userWalletModel)
Expand Down Expand Up @@ -489,7 +494,8 @@ class CommonUserWalletRepository: UserWalletRepository {
}

private func loadModels() {
let savedUserWallets = savedUserWallets(withSensitiveData: true)
var savedUserWallets = savedUserWallets(withSensitiveData: true)
migrateNamesIfNeeded(&savedUserWallets)

models = savedUserWallets.map { userWalletStorageItem in
if let userWallet = CommonUserWalletModel(userWallet: userWalletStorageItem) {
Expand Down Expand Up @@ -520,6 +526,18 @@ class CommonUserWalletRepository: UserWalletRepository {
initializeServices(for: selectedModel)
}

private func migrateNamesIfNeeded(_ wallets: inout [StoredUserWallet]) {
guard !AppSettings.shared.didMigrateUserWalletNames else {
return
}

if let migratedWallets = UserWalletNameIndexationHelper.migratedWallets(wallets) {
UserWalletRepositoryUtil().saveUserWallets(migratedWallets)
}

AppSettings.shared.didMigrateUserWalletNames = true
}

private func sendEvent(_ event: UserWalletRepositoryEvent) {
eventSubject.send(event)
}
Expand Down
1 change: 1 addition & 0 deletions Tangem/Common/Storage/StorageType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ enum StorageType: String {
case pendingBackups = "pending_backups"
case pendingBackupsCurrentID = "pending_backups_current_id"
case forcedDemoCardId = "forced_demo_card_id"
case didMigrateUserWalletNames = "did_migrate_user_wallet_names"
}
15 changes: 14 additions & 1 deletion Tangem/Modules/Main/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ final class MainViewModel: ObservableObject {
private var pendingUserWalletIdsToUpdate: Set<UserWalletId> = []
private var pendingUserWalletModelsToAdd: [UserWalletModel] = []
private var shouldRecreatePagesAfterAddingPendingWalletModels = false
private var walletNameFieldValidator: AlertFieldValidator?

private var shouldDelayBottomSheetVisibility = true
private var isLoggingOut = false

Expand Down Expand Up @@ -176,10 +178,21 @@ final class MainViewModel: ObservableObject {

guard let userWalletModel = userWalletRepository.selectedModel else { return }

let otherWalletNames = userWalletRepository.models.compactMap { model -> String? in
guard model.userWalletId != userWalletModel.userWalletId else { return nil }

return model.name
}

walletNameFieldValidator = AlertFieldValidator { input in
!(otherWalletNames.contains(input) || input.isEmpty)
}

let alert = AlertBuilder.makeAlertControllerWithTextField(
title: Localization.userWalletListRenamePopupTitle,
fieldPlaceholder: Localization.userWalletListRenamePopupPlaceholder,
fieldText: userWalletModel.name
fieldText: userWalletModel.name,
fieldValidator: walletNameFieldValidator
) { newName in
guard userWalletModel.name != newName else { return }

Expand Down
16 changes: 16 additions & 0 deletions TangemApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,8 @@
DA60DD7D2B34572300AC1B79 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA60DD7C2B34572300AC1B79 /* QRScannerView.swift */; };
DA6445262BB17C8100865F7B /* SendDecimalNumberTextFieldPrefixSuffixOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6445252BB17C8100865F7B /* SendDecimalNumberTextFieldPrefixSuffixOptions.swift */; };
DA64C36D27CCAD7E005B5E9A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DA64C36F27CCAD7E005B5E9A /* InfoPlist.strings */; };
DA64DAC92C08BE71000CBC1E /* UserWalletNameIndexationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA64DAC72C08BE71000CBC1E /* UserWalletNameIndexationHelper.swift */; };
DA64DACB2C08BE7B000CBC1E /* UserWalletNameIndexationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA64DACA2C08BE7B000CBC1E /* UserWalletNameIndexationTests.swift */; };
DA682F962A29D08F0083CA79 /* PromotionNetworkModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA682F952A29D08F0083CA79 /* PromotionNetworkModels.swift */; };
DA6915A62A9CE2BD00DB4339 /* Tokens.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA6915A52A9CE2BD00DB4339 /* Tokens.xcassets */; };
DA6C7522292224E00070EEFD /* UserWalletEncryptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6C7521292224E00070EEFD /* UserWalletEncryptionKey.swift */; };
Expand Down Expand Up @@ -2959,6 +2961,8 @@
DA6445252BB17C8100865F7B /* SendDecimalNumberTextFieldPrefixSuffixOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendDecimalNumberTextFieldPrefixSuffixOptions.swift; sourceTree = "<group>"; };
DA64C36E27CCAD7E005B5E9A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DA64C37027CCAD7F005B5E9A /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DA64DAC72C08BE71000CBC1E /* UserWalletNameIndexationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserWalletNameIndexationHelper.swift; sourceTree = "<group>"; };
DA64DACA2C08BE7B000CBC1E /* UserWalletNameIndexationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserWalletNameIndexationTests.swift; sourceTree = "<group>"; };
DA682F952A29D08F0083CA79 /* PromotionNetworkModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionNetworkModels.swift; sourceTree = "<group>"; };
DA6915A52A9CE2BD00DB4339 /* Tokens.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Tokens.xcassets; sourceTree = "<group>"; };
DA69F557298BB3AC004A5A93 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4425,6 +4429,7 @@
5D3F77DB24BF56DC00E8695B /* TangemTests */ = {
isa = PBXGroup;
children = (
DA64DACA2C08BE7B000CBC1E /* UserWalletNameIndexationTests.swift */,
DCA54D8E2C05E0BA00FBF99F /* PriceChangeFormatterTests.swift */,
0A1992A52B5FBDF800344312 /* CurrencyTests.swift */,
5D3F77DC24BF56DC00E8695B /* TangemTests.swift */,
Expand Down Expand Up @@ -7195,6 +7200,14 @@
path = AddressIconView;
sourceTree = "<group>";
};
DA64DAC82C08BE71000CBC1E /* UserWalletNameIndexationHelper */ = {
isa = PBXGroup;
children = (
DA64DAC72C08BE71000CBC1E /* UserWalletNameIndexationHelper.swift */,
);
path = UserWalletNameIndexationHelper;
sourceTree = "<group>";
};
DA7046152AAA180900808FA8 /* Charts */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -7444,6 +7457,7 @@
DC0A57DA2822A4DC0031BECC /* Services */ = {
isa = PBXGroup;
children = (
DA64DAC82C08BE71000CBC1E /* UserWalletNameIndexationHelper */,
DC9632642C514335009B250A /* TokenFinder */,
DC592B912C41A7EA005AB9A7 /* Staking */,
B62C65C62C38D04F00189AD4 /* PushNotifications */,
Expand Down Expand Up @@ -10960,6 +10974,7 @@
EFB7521B2C985F1100CB614C /* CommonFeeIncludedCalculator.swift in Sources */,
DC70C01F2B95F196002205BA /* KeysDerivingMock.swift in Sources */,
EFAD02472A824E6700E9B959 /* TransactionViewModel.swift in Sources */,
DA64DAC92C08BE71000CBC1E /* UserWalletNameIndexationHelper.swift in Sources */,
EF729B722AF3E86300D80205 /* ExpressProvidersSelectorViewModel.swift in Sources */,
DCEC71F52B8683FB00B3847E /* BackupValidator.swift in Sources */,
5D51037927C539B60025EA55 /* ABIError.swift in Sources */,
Expand Down Expand Up @@ -12323,6 +12338,7 @@
0A1992A62B5FBDF800344312 /* CurrencyTests.swift in Sources */,
EF6221B3292B5FCD00D1D4A0 /* DecimalNumberFormatterTests.swift in Sources */,
DC7AC96A292BE2C900580668 /* TangemTests.swift in Sources */,
DA64DACB2C08BE7B000CBC1E /* UserWalletNameIndexationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Loading

0 comments on commit c00779b

Please sign in to comment.