diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 458e56ee5..61a3968bf 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -465,6 +465,8 @@ A5F929AF262C857D00C3E60A /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5F929AE262C857D00C3E60A /* MarkdownKit */; }; A5F929B6262C858700C3E60A /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5F929B5262C858700C3E60A /* MarkdownKit */; }; A5F929B8262C858F00C3E60A /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5F929B7262C858F00C3E60A /* MarkdownKit */; }; + D39AA7892D42745D0069BC73 /* PinPadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39AA7882D42745D0069BC73 /* PinPadView.swift */; }; + D39AA78B2D47A0D80069BC73 /* LocalAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39AA78A2D47A0CC0069BC73 /* LocalAuthenticationService.swift */; }; E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */; }; E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F620EC200900D0CB2D /* SecurityViewController.swift */; }; E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; @@ -1108,6 +1110,8 @@ A5E0422A282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcUnspentTransactionResponse.swift; sourceTree = ""; }; AD258997F050B24C0051CC8D /* Pods-Adamant.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.release.xcconfig"; path = "Target Support Files/Pods-Adamant/Pods-Adamant.release.xcconfig"; sourceTree = ""; }; ADDFD2FA17E41CCBD11A1733 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; + D39AA7882D42745D0069BC73 /* PinPadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinPadView.swift; sourceTree = ""; }; + D39AA78A2D47A0CC0069BC73 /* LocalAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationService.swift; sourceTree = ""; }; E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; E90055F620EC200900D0CB2D /* SecurityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityViewController.swift; sourceTree = ""; }; E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecurityViewController+StayIn.swift"; sourceTree = ""; }; @@ -2404,6 +2408,8 @@ E96D64C72295C44400CA5587 /* Data+utilites.swift */, 64A223D520F760BB005157CB /* Localization.swift */, E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */, + D39AA7882D42745D0069BC73 /* PinPadView.swift */, + D39AA78A2D47A0CC0069BC73 /* LocalAuthenticationService.swift */, E940088A2114F63000CD2D67 /* NSRegularExpression+adamant.swift */, E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */, 6414C18D217DF43100373FA6 /* String+adamant.swift */, @@ -2873,8 +2879,8 @@ isa = PBXNativeTarget; buildConfigurationList = E913C9001FFFA51E001A83F7 /* Build configuration list for PBXNativeTarget "Adamant" */; buildPhases = ( - 9372E0412C9BC178006DF0B3 /* Run Script - Git Data */, 47866E9AB7D201F2CED0064C /* [CP] Check Pods Manifest.lock */, + 9372E0412C9BC178006DF0B3 /* Run Script - Git Data */, E913C8EA1FFFA51D001A83F7 /* Sources */, E913C8EB1FFFA51D001A83F7 /* Frameworks */, E913C8EC1FFFA51D001A83F7 /* Resources */, @@ -3570,6 +3576,7 @@ E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */, E90847332196FEA80095825D /* TransferTransaction+CoreDataProperties.swift in Sources */, 9366588D2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift in Sources */, + D39AA78B2D47A0D80069BC73 /* LocalAuthenticationService.swift in Sources */, E99818942120892F0018C84C /* WalletViewControllerBase.swift in Sources */, 3AA6DF462BA9BEB700EA2E16 /* MediaContentView.swift in Sources */, E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */, @@ -3618,6 +3625,7 @@ E908471B2196FE590095825D /* Adamant.xcdatamodeld in Sources */, E940087B2114ED0600CD2D67 /* EthWalletService.swift in Sources */, 93294B902AAD2C6B00911109 /* SwiftyOnboardOverlay.swift in Sources */, + D39AA7892D42745D0069BC73 /* PinPadView.swift in Sources */, E948E03B20235E2300975D6B /* SettingsFactory.swift in Sources */, 4186B3302941E642006594A3 /* AdmWalletService+DynamicConstants.swift in Sources */, E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */, @@ -3929,7 +3937,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; INFOPLIST_FILE = MessageNotificationContentExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -3940,7 +3948,7 @@ MARKETING_VERSION = 3.10.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.MessageNotificationContentExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev-random1818.MessageNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -3959,7 +3967,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; INFOPLIST_FILE = MessageNotificationContentExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -3969,7 +3977,7 @@ ); MARKETING_VERSION = 3.10.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.MessageNotificationContentExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-random1818.MessageNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -4113,7 +4121,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; DISPLAY_NAME = ADM.Dev; EXCLUDED_SOURCE_FILE_NAMES = ""; GCC_NO_COMMON_BLOCKS = YES; @@ -4124,7 +4132,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 3.10.0; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev-random1818"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -4144,7 +4152,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; DISPLAY_NAME = Adamant; EXCLUDED_SOURCE_FILE_NAMES = Debug.xcassets; GCC_NO_COMMON_BLOCKS = YES; @@ -4155,7 +4163,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 3.10.0; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-random1818"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -4174,7 +4182,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; INFOPLIST_FILE = TransferNotificationContentExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -4185,7 +4193,7 @@ MARKETING_VERSION = 3.10.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.TransferNotificationContentExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev-random1818.TransferNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -4204,7 +4212,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; INFOPLIST_FILE = TransferNotificationContentExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -4214,7 +4222,7 @@ ); MARKETING_VERSION = 3.10.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.TransferNotificationContentExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-random1818TransferNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -4235,7 +4243,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -4246,7 +4254,7 @@ MARKETING_VERSION = 3.10.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.NotificationServiceExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev-random1818.NotificationServiceExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -4265,7 +4273,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = J2L77FMN46; + DEVELOPMENT_TEAM = 23VNGWAPN4; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -4275,7 +4283,7 @@ ); MARKETING_VERSION = 3.10.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.NotificationServiceExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-random1818.NotificationServiceExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; diff --git a/Adamant/Debug.entitlements b/Adamant/Debug.entitlements index 8e1de5348..cf6fd6632 100644 --- a/Adamant/Debug.entitlements +++ b/Adamant/Debug.entitlements @@ -22,7 +22,7 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.device.camera @@ -32,7 +32,7 @@ keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger-dev + $(AppIdentifierPrefix)im.adamant.messenger-dev-random1818 diff --git a/Adamant/Helpers/LocalAuthenticationService.swift b/Adamant/Helpers/LocalAuthenticationService.swift new file mode 100644 index 000000000..67b836811 --- /dev/null +++ b/Adamant/Helpers/LocalAuthenticationService.swift @@ -0,0 +1,186 @@ +// +// LocalAuthenticationHandler.swift +// Adamant +// +// Created by Brian on 27/01/2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import UIKit + +protocol LocalAuthenticationService { + func didEnter(pin: String, pinpadRequest: inout SecurityViewController.PinpadRequest, viewController: UIViewController) + func didTapBiometryButton(pinpadRequest: SecurityViewController.PinpadRequest, viewController: UIViewController) + func didTapCancel(pinpadRequest: SecurityViewController.PinpadRequest, viewController: UIViewController) +} + +final class LocalAuthenticationServiceImpl: LocalAuthenticationService { + + let accountService: AccountService + let localAuth: LocalAuthentication + + init(accountService: AccountService, localAuth: LocalAuthentication) { + self.accountService = accountService + self.localAuth = localAuth + } + + func didEnter(pin: String, pinpadRequest: inout SecurityViewController.PinpadRequest, viewController: UIViewController) { + switch pinpadRequest { + + // MARK: User has entered new pin first time. Request re-enter pin + case .createPin: + pinpadRequest = .reenterPin(pin: pin) +// pinpad.commentLabel.text = String.adamant.pinpad.reenterPin +// pinpad.clearPin() + return + + // MARK: User has reentered pin. Save pin. + case .reenterPin(let pinToVerify): + guard pin == pinToVerify else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() + break + } + + let result = accountService.setStayLoggedIn(pin: pin) + Task { @MainActor in + switch result { + case .success: return +// self.pinpadRequest = nil +// if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// row.evaluateHidden() +// } + +// if let section = self.form.sectionBy(tag: Sections.notifications.tag) { +// section.evaluateHidden() +// } + +// if let section = self.form.sectionBy(tag: Sections.aboutNotificationTypes.tag) { +// section.evaluateHidden() +// } + +// pinpad.dismiss(animated: true, completion: nil) + + case .failure(let error): return +// self.dialogService.showRichError(error: error) + } + } + + // MARK: Users want to turn off the pin. Validate and turn off. + case .turnOffPin: + guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() + break + } + + accountService.dropSavedAccount() + +// pinpad.dismiss(animated: true, completion: nil) + + // MARK: User wants to turn on biometry + case .turnOnBiometry: + guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() + break + } + + accountService.updateUseBiometry(true) +// pinpad.dismiss(animated: true, completion: nil) + + // MARK: User wants to turn off biometry + case .turnOffBiometry: + guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() + break + } + + accountService.updateUseBiometry(false) +// pinpad.dismiss(animated: true, completion: nil) + + default: return +// pinpad.dismiss(animated: true, completion: nil) + } + + } + + func didTapBiometryButton(pinpadRequest: SecurityViewController.PinpadRequest, viewController: UIViewController) { + Task { + switch pinpadRequest { + // MARK: User wants to turn of StayIn with his face. Or finger. + case .turnOffPin: + let result = await localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff) + switch result { + case .success: + self.accountService.dropSavedAccount() + +// if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// row.evaluateHidden() +// } + +// if let row = self.form.rowBy(tag: Rows.notifications.tag) { +// row.evaluateHidden() +// } + +// pinpad.dismiss(animated: true, completion: nil) + + case .cancel: break + case .fallback: break + case .failed: break + case .biometryLockout: break + } + default: + return + } + } + } + + func didTapCancel(pinpadRequest: SecurityViewController.PinpadRequest, viewController: UIViewController) { +// MainActor.assumeIsolatedSafe { + switch pinpadRequest { + + // MARK: User canceled turning on StayIn + case .createPin, .reenterPin(pin: _): + return +// if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { +// row.value = false +// row.updateCell() +// } + + // MARK: User canceled turning off StayIn + case .turnOffPin: +// if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { +// row.value = true +// row.updateCell() +// } + return + // MARK: User canceled Biometry On + case .turnOnBiometry: +// if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// } + return + // MARK: User canceled Biometry Off + case .turnOffBiometry: +// if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { +// row.value = true +// row.updateCell() +// } + return + default: + break + } + +// pinpadRequest = nil +// pinpad.dismiss(animated: true, completion: nil) +// } + } +} + diff --git a/Adamant/Helpers/MyLittlePinpad+adamant.swift b/Adamant/Helpers/MyLittlePinpad+adamant.swift index 34c06bda6..6a7874d00 100644 --- a/Adamant/Helpers/MyLittlePinpad+adamant.swift +++ b/Adamant/Helpers/MyLittlePinpad+adamant.swift @@ -7,7 +7,8 @@ // import Foundation -import MyLittlePinpad +//import MyLittlePinpad +import SwiftUI extension String.adamant { enum pinpad { @@ -20,64 +21,88 @@ extension String.adamant { } } -extension PinpadBiometryButtonType { - var localAuthType: BiometryType { - switch self { - case .hidden: - return .none - - case .faceID: - return .faceID - - case .touchID: - return .touchID - } - } -} +//extension PinpadBiometryButtonType { +// var localAuthType: BiometryType { +// switch self { +// case .hidden: +// return .none +// +// case .faceID: +// return .faceID +// +// case .touchID: +// return .touchID +// } +// } +//} -extension BiometryType { - var pinpadButtonType: PinpadBiometryButtonType { - switch self { - case .none: - return .hidden - - case .faceID: - return .faceID - - case .touchID: - return .touchID - } - } -} +//extension BiometryType { +// var pinpadButtonType: PinpadBiometryButtonType { +// switch self { +// case .none: +// return .hidden +// +// case .faceID: +// return .faceID +// +// case .touchID: +// return .touchID +// } +// } +//} -extension PinpadViewController { - static func adamantPinpad(biometryButton: PinpadBiometryButtonType) -> PinpadViewController { - let pinpad = PinpadViewController.instantiateFromResourceNib() - - pinpad.bordersColor = UIColor.adamant.secondary - pinpad.setColor(UIColor.adamant.primary, for: .normal) - pinpad.buttonsHighlightedColor = UIColor.adamant.pinpadHighlightButton - pinpad.buttonsFont = UIFont.adamantPrimary(ofSize: pinpad.buttonsFont.pointSize, weight: .light) - - pinpad.placeholdersSize = 15 +extension UIViewController { + func adamantPinpad(biometryButton: BiometryType, onSuccess: (String) -> Void) -> UIViewController { + let pinPadView = PinPadViewRepresentable( + pinLength: 6, + mode: .createPin, + validatePin: { $0 == "123456" }, + onSuccess: { print("Pin validated!") }, + onCancel: { print("Pin entry canceled.") } + ) - if pinpad.view.frame.height > 600 { - pinpad.buttonsSize = 75 - pinpad.buttonsSpacing = 20 - pinpad.placeholderViewHeight = 50 - } else {// iPhone 5 - pinpad.buttonsSize = 70 - pinpad.buttonsSpacing = 15 - pinpad.placeholderViewHeight = 25 - pinpad.bottomSpacing = 24 - pinpad.pinpadToCancelSpacing = 14 - } + let hostingController = UIHostingController(rootView: pinPadView) + addChild(hostingController) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingController.view) - pinpad.placeholderActiveColor = UIColor.adamant.pinpadHighlightButton - pinpad.biometryButtonType = biometryButton - pinpad.cancelButton.setTitle(String.adamant.alert.cancel, for: .normal) - pinpad.pinDigits = 6 + NSLayoutConstraint.activate([ + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) - return pinpad + hostingController.didMove(toParent: self) + return hostingController +// let pinpad = PinpadViewController.instantiateFromResourceNib() +// +// pinpad.bordersColor = UIColor.adamant.secondary +// pinpad.setColor(UIColor.adamant.primary, for: .normal) +// pinpad.buttonsHighlightedColor = UIColor.adamant.pinpadHighlightButton +// pinpad.buttonsFont = UIFont.adamantPrimary(ofSize: pinpad.buttonsFont.pointSize, weight: .light) +// +// pinpad.placeholdersSize = 15 +// +// if pinpad.view.frame.height > 600 { +// pinpad.buttonsSize = 75 +// pinpad.buttonsSpacing = 20 +// pinpad.placeholderViewHeight = 50 +// } else {// iPhone 5 +// pinpad.buttonsSize = 70 +// pinpad.buttonsSpacing = 15 +// pinpad.placeholderViewHeight = 25 +// pinpad.bottomSpacing = 24 +// pinpad.pinpadToCancelSpacing = 14 +// } +// +// pinpad.placeholderActiveColor = UIColor.adamant.pinpadHighlightButton +// pinpad.biometryButtonType = biometryButton +// pinpad.cancelButton.setTitle(String.adamant.alert.cancel, for: .normal) +// pinpad.pinDigits = 6 +// +// return pinpad } } + + diff --git a/Adamant/Helpers/PinPadView.swift b/Adamant/Helpers/PinPadView.swift new file mode 100644 index 000000000..8bb9fc606 --- /dev/null +++ b/Adamant/Helpers/PinPadView.swift @@ -0,0 +1,339 @@ +// +// StyledPinpadView.swift +// Adamant +// +// Created by Brian on 23/01/2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import SwiftUI + +struct PinPadViewRepresentable: UIViewRepresentable { + let pinLength: Int + let mode: PinPadViewModel.Mode + let validatePin: (String) -> Bool + let onSuccess: () -> Void + let onCancel: () -> Void + + func makeUIView(context: Context) -> UIView { + let hostingController = UIHostingController( + rootView: PinPadView( + viewModel: + PinPadViewModel( + pinLength: pinLength, + mode: mode, + validatePin: validatePin, + onSuccess: onSuccess, + onCancel: onCancel + ) + ) + ) + return hostingController.view + } + + func updateUIView(_ uiView: UIView, context: Context) { + // Handle updates to the view if needed + } +} + +class PinPadViewModel: ObservableObject { + enum Mode { + case enterPin + case createPin + case wrongPinEntered + case pinNotMatching + case reenterPin + case pinCreated + case pinValidated + case turnOffPin + case turnOnBiometry + case turnOffBiometry + } + @Published private(set) var mode: Mode + @Published private var enteredPin: String = "" + @Published private var previousEntry: String = "" + @Published private(set) var title: String = "" + @Published private(set) var titleColor: Color = .white + let pinLength: Int + private let validatePin: (String) -> Bool + let onSuccess: () -> Void + let onCancel: () -> Void + + init( + pinLength: Int, + mode: Mode, + validatePin: @escaping (String) -> Bool, + onSuccess: @escaping () -> Void, + onCancel: @escaping () -> Void + ) { + self.pinLength = pinLength + self.validatePin = validatePin + self.onSuccess = onSuccess + self.onCancel = onCancel + self.mode = mode + update(mode: mode) + } + + func append(_ character: String) { + enteredPin.append(character) + } + + func removeLast() { + enteredPin.removeLast() + } + + func update(mode: Mode) { + let title: String + let titleColor: Color + switch mode { + case .createPin: + title = String.adamant.pinpad.createPin + titleColor = .primary + case .enterPin: + title = "Login into ADAMANT" + titleColor = .primary + case .reenterPin: + title = String.adamant.pinpad.reenterPin + titleColor = .primary + case .wrongPinEntered: + title = "Wrong PIN entered!" + titleColor = .red + case .pinNotMatching: + title = "PIN doesn't match!" + titleColor = .red + case .pinValidated: + title = "Success!" + titleColor = .green + case .pinCreated: + title = "PIN created!" + titleColor = .green + case .turnOffPin: + title = String.adamant.security.stayInTurnOff + titleColor = .primary + case .turnOffBiometry: + title = String.adamant.security.biometryOffReason + titleColor = .primary + case .turnOnBiometry: + title = String.adamant.security.biometryOnReason + titleColor = .primary + } + self.title = title + self.titleColor = titleColor + self.mode = mode + } + + func updateCache(mode: Mode) { + switch mode { + case .createPin: + enteredPin.removeAll() + previousEntry.removeAll() + case .enterPin: + return + case .reenterPin: + previousEntry = enteredPin + enteredPin.removeAll() + case .wrongPinEntered: + enteredPin.removeAll() + case .pinNotMatching: + enteredPin.removeAll() + previousEntry.removeAll() + case .pinValidated: + return + case .pinCreated: + return + case .turnOffBiometry, .turnOffPin, .turnOnBiometry: + return + } + self.title = title + self.titleColor = titleColor + } + + func isValid(mode: Mode) -> Bool { + switch mode { + case .reenterPin: + previousEntry == enteredPin + case .wrongPinEntered, + .createPin, + .enterPin, + .pinNotMatching, + .pinValidated, + .pinCreated, + .turnOffBiometry, + .turnOffPin, + .turnOnBiometry: + true + } + } + + var isEmpty: Bool { + enteredPin.isEmpty + } + + var isPinEnteredCompletely: Bool { + enteredPin.count == pinLength + } + + func hasNumber(at index: Int) -> Bool { + index < enteredPin.count + } + + var isPinValid: Bool { + validatePin(enteredPin) + } +} + +// swiftlint:disable multiple_closures_with_trailing_closure +struct PinPadView: View { + @StateObject var viewModel: PinPadViewModel + + var body: some View { + VStack { + Spacer() + Text(viewModel.title) + .foregroundColor(viewModel.titleColor) + .textCase(nil) + .font(.body) + .padding(.top, 30) + + HStack(spacing: 18) { + ForEach(0.. some View { + HStack(spacing: 25) { + if showsDeleteButton { + Circle() + .frame(width: 80, height: 80) + .foregroundColor(.clear) + } + ForEach(from...to, id: \.self) { number in + Button(action: { + handlePinInput("\(number)") + }) { + Circle() + .frame(width: 80, height: 80) + .overlay( + Text("\(number)") + .foregroundColor(Color(UIColor.adamant.primary)) + .font(Font(UIFont.adamantPrimary(ofSize: 35, weight: .light))) + ) + .foregroundColor(.clear) + .overlay(Circle().stroke(Color(UIColor.adamant.secondary), lineWidth: 0.5)) + } + } + if showsDeleteButton { + Button(action: deleteLastDigit) { + Circle() + .frame(width: 80, height: 80) + .overlay( + Image(systemName: "delete.left") + .foregroundColor(Color(UIColor.adamant.primary)) + .font(.title) + ) + .foregroundColor(.clear) + .overlay(Circle().stroke(Color(UIColor.adamant.secondary), lineWidth: 0.5)) + } + } + } + .frame(height: 90) + } + + private func handlePinInput(_ digit: String) { + viewModel.append(digit) + if viewModel.isPinEnteredCompletely { + switch viewModel.mode { + case .enterPin: + if viewModel.isPinValid { + viewModel.update(mode: .pinValidated) + viewModel.onSuccess() + } else { + viewModel.update(mode: .wrongPinEntered) + Task { @MainActor in + viewModel.update(mode: .enterPin) + viewModel.updateCache(mode: .enterPin) + } + } + case .createPin: + viewModel.update(mode: .reenterPin) + Task { @MainActor in + try await Task.sleep(nanoseconds: 300_000_000) + viewModel.updateCache(mode: .reenterPin) + } + case .reenterPin: + if viewModel.isValid(mode: .reenterPin) { + viewModel.update(mode: .pinCreated) + Task { @MainActor in + try await Task.sleep(nanoseconds: 100_000_000) + viewModel.updateCache(mode: .pinCreated) + viewModel.onSuccess() + } + } else { + viewModel.update(mode: .pinNotMatching) + Task { @MainActor in + try await Task.sleep(nanoseconds: 500_000_000) + viewModel.update(mode: .createPin) + viewModel.updateCache(mode: .createPin) + } + } + case .turnOffBiometry, + .turnOffPin, + .turnOnBiometry, + .pinCreated, + .wrongPinEntered, + .pinNotMatching, + .pinValidated: + return + } + } + } + + private func deleteLastDigit() { + guard !viewModel.isEmpty else { return } + viewModel.removeLast() + } +} + +#if DEBUG +#Preview { + PinPadView( + viewModel: + PinPadViewModel( + pinLength: 6, + mode: .createPin, + validatePin: + { _ in + true + }, onSuccess: { + + }, onCancel: { + + } + ) + ) +} +#endif diff --git a/Adamant/Helpers/UITextField+adamant.swift b/Adamant/Helpers/UITextField+adamant.swift index 9081e2101..5c67d49e6 100644 --- a/Adamant/Helpers/UITextField+adamant.swift +++ b/Adamant/Helpers/UITextField+adamant.swift @@ -144,24 +144,38 @@ extension UITextField { } extension UITextField { + + // MARK: - Password toggle button + + static var buttonContainerHeight: CGFloat { 28 } + static var buttonImageEdgeInsets: UIEdgeInsets { + UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3) + } + func enablePasswordToggle() { - let button = UIButton(type: .custom) - updatePasswordToggleImage(button) - button.addTarget(self, action: #selector(togglePasswordView(_:)), for: .touchUpInside) + let button = makePasswordButton() - let contanerView = UIView() - contanerView.addSubview(button) + let containerView = UIView() + containerView.addSubview(button) button.snp.makeConstraints { make in - make.directionalEdges.equalToSuperview().inset(3) + make.directionalEdges.equalToSuperview() } - contanerView.snp.makeConstraints { make in - make.size.equalTo(28) + containerView.snp.makeConstraints { make in + make.size.equalTo(UITextField.buttonContainerHeight) } - rightView = contanerView + rightView = containerView rightViewMode = .always } + func makePasswordButton() -> UIButton { + let button = UIButton(type: .custom) + button.imageEdgeInsets = UITextField.buttonImageEdgeInsets + updatePasswordToggleImage(button) + button.addTarget(self, action: #selector(togglePasswordView(_:)), for: .touchUpInside) + return button + } + private func updatePasswordToggleImage(_ button: UIButton) { let imageName = isSecureTextEntry ? "eye_close" : "eye_open" button.setImage(.asset(named: imageName), for: .normal) diff --git a/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift b/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift index 2f170945c..772a45a85 100644 --- a/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift +++ b/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift @@ -8,7 +8,7 @@ import Foundation import Eureka -import MyLittlePinpad +//import MyLittlePinpad import CommonKit extension AccountViewController { @@ -19,257 +19,250 @@ extension AccountViewController { if enabled { // Create pin and turn on Stay In pinpadRequest = .createPin - let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) - pinpad.commentLabel.text = String.adamant.pinpad.createPin - pinpad.commentLabel.isHidden = false - pinpad.delegate = self + let pinpad = adamantPinpad(biometryButton: .none) { _ in } +// pinpad.commentLabel.text = String.adamant.pinpad.createPin +// pinpad.commentLabel.isHidden = false +// pinpad.delegate = self pinpad.modalPresentationStyle = .overFullScreen - pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor - setColors(for: pinpad) - present(pinpad, animated: true, completion: nil) +// pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor +// setColors(for: pinpad) +// present(pinpad, animated: true, completion: nil) } else { // Validate pin and turn off Stay In pinpadRequest = .turnOffPin - let biometryButton: PinpadBiometryButtonType = accountService.useBiometry ? localAuth.biometryType.pinpadButtonType : .hidden - let pinpad = PinpadViewController.adamantPinpad(biometryButton: biometryButton) - pinpad.commentLabel.text = String.adamant.security.stayInTurnOff - pinpad.commentLabel.isHidden = false - pinpad.delegate = self + let biometryButton: BiometryType = accountService.useBiometry ? localAuth.biometryType : .none + let pinpad = adamantPinpad(biometryButton: biometryButton) { _ in } +// pinpad.commentLabel.text = String.adamant.security.stayInTurnOff +// pinpad.commentLabel.isHidden = false +// pinpad.delegate = self pinpad.modalPresentationStyle = .overFullScreen - setColors(for: pinpad) - present(pinpad, animated: true, completion: nil) +// setColors(for: pinpad) +// present(pinpad, animated: true, completion: nil) } } // MARK: Use biometry - func setBiometry(enabled: Bool) { + func setBiometry(enabled: Bool) { guard showLoggedInOptions, accountService.hasStayInAccount, accountService.useBiometry != enabled else { return } - let reason = enabled ? String.adamant.security.biometryOnReason : String.adamant.security.biometryOffReason - localAuth.authorizeUser(reason: reason) { result in - Task { @MainActor [weak self] in + Task { @MainActor [weak self] in + guard let self else { return } + let reason = enabled ? String.adamant.security.biometryOnReason : String.adamant.security.biometryOffReason + let result = await self.localAuth.authorizeUser(reason: reason) + switch result { case .success: - self?.dialogService.showSuccess(withMessage: String.adamant.alert.done) - self?.accountService.updateUseBiometry(enabled) + self.dialogService.showSuccess(withMessage: String.adamant.alert.done) + self.accountService.updateUseBiometry(enabled) case .cancel: - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = self?.accountService.useBiometry + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = self.accountService.useBiometry row.updateCell() } case .fallback: - let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) + let pinpad = adamantPinpad(biometryButton: .none) { _ in } if enabled { - pinpad.commentLabel.text = String.adamant.security.biometryOnReason - self?.pinpadRequest = .turnOnBiometry +// pinpad.commentLabel.text = String.adamant.security.biometryOnReason + self.pinpadRequest = .turnOnBiometry } else { - pinpad.commentLabel.text = String.adamant.security.biometryOffReason - self?.pinpadRequest = .turnOffBiometry +// pinpad.commentLabel.text = String.adamant.security.biometryOffReason + self.pinpadRequest = .turnOffBiometry } - pinpad.commentLabel.isHidden = false - pinpad.delegate = self +// pinpad.commentLabel.isHidden = false +// pinpad.delegate = self pinpad.modalPresentationStyle = .overFullScreen - self?.setColors(for: pinpad) - self?.present(pinpad, animated: true, completion: nil) +// self.setColors(for: pinpad) +// self.present(pinpad, animated: true, completion: nil) case .failed: - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - if let value = self?.accountService.useBiometry { - row.value = value - } else { - row.value = false - } - + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = self.accountService.useBiometry row.updateCell() row.evaluateHidden() } - if let row = self?.form.rowBy(tag: Rows.notifications.tag) { + if let row = self.form.rowBy(tag: Rows.notifications.tag) { row.evaluateHidden() } + case .biometryLockout: + return } - } } } - func setColors(for pinpad: PinpadViewController) { - pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor - pinpad.buttonsBackgroundColor = UIColor.adamant.backgroundColor - pinpad.view.subviews.forEach { view in - view.subviews.forEach { _view in - if _view.backgroundColor == .white { - _view.backgroundColor = UIColor.adamant.backgroundColor - } - } - } - pinpad.commentLabel.backgroundColor = UIColor.adamant.backgroundColor - } +// func setColors(for pinpad: PinpadViewController) { +// pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor +// pinpad.buttonsBackgroundColor = UIColor.adamant.backgroundColor +// pinpad.view.subviews.forEach { view in +// view.subviews.forEach { _view in +// if _view.backgroundColor == .white { +// _view.backgroundColor = UIColor.adamant.backgroundColor +// } +// } +// } +// pinpad.commentLabel.backgroundColor = UIColor.adamant.backgroundColor +// } } // MARK: - PinpadViewControllerDelegate -extension AccountViewController: PinpadViewControllerDelegate { - nonisolated func pinpad(_ pinpad: PinpadViewController, didEnterPin pin: String) { - MainActor.assumeIsolatedSafe { - switch pinpadRequest { - - // MARK: User has entered new pin first time. Request re-enter pin - case .createPin?: - pinpadRequest = .reenterPin(pin: pin) - pinpad.commentLabel.text = String.adamant.pinpad.reenterPin - pinpad.clearPin() - return - - // MARK: User has reentered pin. Save pin. - case .reenterPin(let pinToVerify)?: - guard pin == pinToVerify else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.setStayLoggedIn(pin: pin) { [weak self] result in - Task { @MainActor in - switch result { - case .success: - self?.pinpadRequest = nil - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let row = self?.form.rowBy(tag: Rows.notifications.tag) { - row.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) - - case .failure(let error): - self?.dialogService.showRichError(error: error) - } - } - } - - // MARK: Users want to turn off the pin. Validate and turn off. - case .turnOffPin?: - guard accountService.validatePin(pin) else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.dropSavedAccount() - - pinpad.dismiss(animated: true, completion: nil) - - // MARK: User wants to turn on biometry - case .turnOnBiometry?: - guard accountService.validatePin(pin) else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.updateUseBiometry(true) - pinpad.dismiss(animated: true, completion: nil) - - // MARK: User wants to turn off biometry - case .turnOffBiometry?: - guard accountService.validatePin(pin) else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.updateUseBiometry(false) - pinpad.dismiss(animated: true, completion: nil) - - default: - pinpad.dismiss(animated: true, completion: nil) - } - } - } +//extension AccountViewController: PinpadViewControllerDelegate { +// nonisolated func pinpad(_ pinpad: PinpadViewController, didEnterPin pin: String) { +// MainActor.assumeIsolatedSafe { +// switch pinpadRequest { +// +// // MARK: User has entered new pin first time. Request re-enter pin +// case .createPin?: +// pinpadRequest = .reenterPin(pin: pin) +// pinpad.commentLabel.text = String.adamant.pinpad.reenterPin +// pinpad.clearPin() +// return +// +// // MARK: User has reentered pin. Save pin. +// case .reenterPin(let pinToVerify)?: +// guard pin == pinToVerify else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// let result = accountService.setStayLoggedIn(pin: pin) +// Task { @MainActor in +// switch result { +// case .success: +// self.pinpadRequest = nil +// if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// row.evaluateHidden() +// } +// +// if let row = self.form.rowBy(tag: Rows.notifications.tag) { +// row.evaluateHidden() +// } +// +// pinpad.dismiss(animated: true, completion: nil) +// +// case .failure(let error): +// self.dialogService.showRichError(error: error) +// } +// } +// +// // MARK: Users want to turn off the pin. Validate and turn off. +// case .turnOffPin?: +// guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// accountService.dropSavedAccount() +// +// pinpad.dismiss(animated: true, completion: nil) +// +// // MARK: User wants to turn on biometry +// case .turnOnBiometry?: +// guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// accountService.updateUseBiometry(true) +// pinpad.dismiss(animated: true, completion: nil) +// +// // MARK: User wants to turn off biometry +// case .turnOffBiometry?: +// guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// accountService.updateUseBiometry(false) +// pinpad.dismiss(animated: true, completion: nil) +// +// default: +// pinpad.dismiss(animated: true, completion: nil) +// } +// } +// } - nonisolated func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { - MainActor.assumeIsolatedSafe { - switch pinpadRequest { - - // MARK: User wants to turn of StayIn with his face. Or finger. - case .turnOffPin?: - localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff, completion: { [weak self] result in - switch result { - case .success: - self?.accountService.dropSavedAccount() - - DispatchQueue.main.async { - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let row = self?.form.rowBy(tag: Rows.notifications.tag) { - row.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) - } - - case .cancel: break - case .fallback: break - case .failed: break - } - }) - - default: - return - } - } - } +// func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { +// Task { @MainActor in +// switch pinpadRequest { +// // MARK: User wants to turn of StayIn with his face. Or finger. +// case .turnOffPin?: +// let result = await localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff) +// switch result { +// case .success: +// self.accountService.dropSavedAccount() +// +// if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// row.evaluateHidden() +// } +// +// if let row = self.form.rowBy(tag: Rows.notifications.tag) { +// row.evaluateHidden() +// } +// +// pinpad.dismiss(animated: true, completion: nil) +// +// case .cancel: break +// case .fallback: break +// case .failed: break +// case .biometryLockout: break +// } +// default: +// return +// } +// } +// } - nonisolated func pinpadDidCancel(_ pinpad: PinpadViewController) { - MainActor.assumeIsolatedSafe { - switch pinpadRequest { - - // MARK: User canceled turning on StayIn - case .createPin?, .reenterPin(pin: _)?: - if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { - row.value = false - row.updateCell() - } - - // MARK: User canceled turning off StayIn - case .turnOffPin?: - if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { - row.value = true - row.updateCell() - } - - // MARK: User canceled Biometry On - case .turnOnBiometry?: - if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - } - - // MARK: User canceled Biometry Off - case .turnOffBiometry?: - if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { - row.value = true - row.updateCell() - } - - default: - break - } - - pinpadRequest = nil - pinpad.dismiss(animated: true, completion: nil) - } - } -} +// func pinpadDidCancel(_ pinpad: PinpadViewController) { +// MainActor.assumeIsolatedSafe { +// switch pinpadRequest { +// +// // MARK: User canceled turning on StayIn +// case .createPin?, .reenterPin(pin: _)?: +// if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { +// row.value = false +// row.updateCell() +// } +// +// // MARK: User canceled turning off StayIn +// case .turnOffPin?: +// if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { +// row.value = true +// row.updateCell() +// } +// +// // MARK: User canceled Biometry On +// case .turnOnBiometry?: +// if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// } +// +// // MARK: User canceled Biometry Off +// case .turnOffBiometry?: +// if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { +// row.value = true +// row.updateCell() +// } +// +// default: +// break +// } +// +// pinpadRequest = nil +// pinpad.dismiss(animated: true, completion: nil) +// } +// } +//} diff --git a/Adamant/Modules/ChatsList/ChatListViewController.swift b/Adamant/Modules/ChatsList/ChatListViewController.swift index 6783bb9f6..b7b33ebc5 100644 --- a/Adamant/Modules/ChatsList/ChatListViewController.swift +++ b/Adamant/Modules/ChatsList/ChatListViewController.swift @@ -1528,7 +1528,7 @@ extension ChatListViewController { } } -private extension State { +private extension DataProviderState { var isUpdating: Bool { switch self { case .updating: true diff --git a/Adamant/Modules/Login/LoginViewController+Pinpad.swift b/Adamant/Modules/Login/LoginViewController+Pinpad.swift index 7a634aad1..18f85d841 100644 --- a/Adamant/Modules/Login/LoginViewController+Pinpad.swift +++ b/Adamant/Modules/Login/LoginViewController+Pinpad.swift @@ -13,26 +13,27 @@ import CommonKit extension LoginViewController { /// Shows pinpad in main.async queue func loginWithPinpad() { - let button: PinpadBiometryButtonType = accountService.useBiometry ? localAuth.biometryType.pinpadButtonType : .hidden + let button: BiometryType = accountService.useBiometry ? localAuth.biometryType : .none - DispatchQueue.main.async { [weak self] in - let pinpad = PinpadViewController.adamantPinpad(biometryButton: button) - pinpad.commentLabel.text = String.adamant.login.loginIntoPrevAccount - pinpad.commentLabel.isHidden = false - pinpad.delegate = self +// DispatchQueue.main.async { [weak self] in +// guard let self else { return } + let pinpad = adamantPinpad(biometryButton: button) { _ in } +// pinpad.commentLabel.text = String.adamant.login.loginIntoPrevAccount +// pinpad.commentLabel.isHidden = false +// pinpad.delegate = self pinpad.modalPresentationStyle = .overFullScreen - pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor - pinpad.buttonsBackgroundColor = UIColor.adamant.backgroundColor - pinpad.view.subviews.forEach { view in - view.subviews.forEach { _view in - if _view.backgroundColor == .white { - _view.backgroundColor = UIColor.adamant.backgroundColor - } - } - } - pinpad.commentLabel.backgroundColor = UIColor.adamant.backgroundColor - self?.present(pinpad, animated: true, completion: nil) - } +// pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor +// pinpad.buttonsBackgroundColor = UIColor.adamant.backgroundColor +// pinpad.view.subviews.forEach { view in +// view.subviews.forEach { _view in +// if _view.backgroundColor == .white { +// _view.backgroundColor = UIColor.adamant.backgroundColor +// } +// } +// } +// pinpad.commentLabel.backgroundColor = UIColor.adamant.backgroundColor +// present(pinpad, animated: true, completion: nil) +// } } /// Request user biometry authentication @@ -43,21 +44,17 @@ extension LoginViewController { return } - localAuth.authorizeUser(reason: .adamant.login.loginIntoPrevAccount) { result in - Task { @MainActor [weak self] in - switch result { - case .success: - self?.loginIntoSavedAccount() - - case .fallback: - self?.loginWithPinpad() - - case .cancel: - break - - case .failed: - break - } + Task { @MainActor in + let result = await localAuth.authorizeUser(reason: .adamant.login.loginIntoPrevAccount) + switch result { + case .success: + self.loginIntoSavedAccount() + + case .fallback: + self.loginWithPinpad() + + case .cancel, .failed, .biometryLockout: + break } } } @@ -133,15 +130,14 @@ extension LoginViewController: PinpadViewControllerDelegate { nonisolated func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { Task { @MainActor in - localAuth.authorizeUser(reason: String.adamant.login.loginIntoPrevAccount, completion: { [weak self] result in - switch result { - case .success: - self?.loginIntoSavedAccount() - - case .fallback, .cancel, .failed: - break - } - }) + let result = await localAuth.authorizeUser(reason: String.adamant.login.loginIntoPrevAccount) + switch result { + case .success: + self.loginIntoSavedAccount() + + case .fallback, .cancel, .failed, .biometryLockout: + break + } } } diff --git a/Adamant/Modules/Login/LoginViewController.swift b/Adamant/Modules/Login/LoginViewController.swift index 29199bcb3..fb5be3ee3 100644 --- a/Adamant/Modules/Login/LoginViewController.swift +++ b/Adamant/Modules/Login/LoginViewController.swift @@ -230,7 +230,7 @@ final class LoginViewController: FormViewController { $0.tag = Rows.passphrase.tag $0.placeholder = Rows.passphrase.localized $0.placeholderColor = UIColor.adamant.secondary - $0.cell.textField.enablePasswordToggle() + $0.cell.textField.enablePasteButtonAndPasswordToggle() $0.keyboardReturnType = KeyboardReturnTypeConfiguration(nextKeyboardType: .go, defaultKeyboardType: .go) } @@ -414,6 +414,17 @@ final class LoginViewController: FormViewController { versionFooterView.sizeToFit() } + // MARK: - FormViewController + + override func textInputShouldReturn(_ textInput: UITextInput, cell: Cell) -> Bool { + let result = super.textInputShouldReturn(textInput, cell: cell) + if cell.row.tag == Rows.passphrase.tag, let passphrase = cell.row.value as? String { + loginWith(passphrase: passphrase) + } + + return result + } + // MARK: - Other private func setColors() { @@ -514,3 +525,50 @@ extension LoginViewController: ButtonsStripeViewDelegate { } } } + +// MARK: UITextField + extensions + +private extension UITextField { + func enablePasteButtonAndPasswordToggle() { + let passwordToggleButton = makePasswordButton() + let pasteButton = makePasteButton() + + let containerView = UIView() + let buttonStack = UIStackView(arrangedSubviews: [pasteButton, passwordToggleButton]) + buttonStack.axis = .horizontal + buttonStack.spacing = 4 + containerView.addSubview(buttonStack) + buttonStack.snp.makeConstraints { make in + make.directionalEdges.equalToSuperview() + } + + containerView.snp.makeConstraints { make in + make.height.equalTo(UITextField.buttonContainerHeight) + } + + pasteButton.snp.makeConstraints { make in + make.width.equalTo(pasteButton.snp.height) + } + + passwordToggleButton.snp.makeConstraints { make in + make.width.equalTo(passwordToggleButton.snp.height) + } + + rightView = containerView + rightViewMode = .always + } + + func makePasteButton() -> UIButton { + let button = UIButton(type: .custom) + button.imageEdgeInsets = UITextField.buttonImageEdgeInsets + button.setImage(.asset(named: "clipboard"), for: .normal) + button.addTarget(self, action: #selector(pasteFromPasteboard(_:)), for: .touchUpInside) + return button + } + + @objc func pasteFromPasteboard(_ sender: UIButton) { + if let pasteboardText = UIPasteboard.general.string { + self.text = pasteboardText + } + } +} diff --git a/Adamant/Modules/Settings/SecurityViewController+StayIn.swift b/Adamant/Modules/Settings/SecurityViewController+StayIn.swift index af2f0db0a..a8d61eee1 100644 --- a/Adamant/Modules/Settings/SecurityViewController+StayIn.swift +++ b/Adamant/Modules/Settings/SecurityViewController+StayIn.swift @@ -19,21 +19,21 @@ extension SecurityViewController { if enabled { // Create pin and turn on Stay In pinpadRequest = .createPin - let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) - pinpad.commentLabel.text = String.adamant.pinpad.createPin - pinpad.commentLabel.isHidden = false - pinpad.delegate = self + let pinpad = adamantPinpad(biometryButton: .none) { _ in } +// pinpad.commentLabel.text = String.adamant.pinpad.createPin +// pinpad.commentLabel.isHidden = false +// pinpad.delegate = self pinpad.modalPresentationStyle = .overFullScreen - present(pinpad, animated: true, completion: nil) +// present(pinpad, animated: true, completion: nil) } else { // Validate pin and turn off Stay In pinpadRequest = .turnOffPin - let biometryButton: PinpadBiometryButtonType = accountService.useBiometry ? localAuth.biometryType.pinpadButtonType : .hidden - let pinpad = PinpadViewController.adamantPinpad(biometryButton: biometryButton) - pinpad.commentLabel.text = String.adamant.security.stayInTurnOff - pinpad.commentLabel.isHidden = false - pinpad.delegate = self + let biometryButton: BiometryType = accountService.useBiometry ? localAuth.biometryType : .none + let pinpad = adamantPinpad(biometryButton: biometryButton) { _ in } +// pinpad.commentLabel.text = String.adamant.security.stayInTurnOff +// pinpad.commentLabel.isHidden = false +// pinpad.delegate = self pinpad.modalPresentationStyle = .overFullScreen - present(pinpad, animated: true, completion: nil) +// present(pinpad, animated: true, completion: nil) } } @@ -44,11 +44,12 @@ extension SecurityViewController { } let reason = enabled ? String.adamant.security.biometryOnReason : String.adamant.security.biometryOffReason - localAuth.authorizeUser(reason: reason) { [weak self] result in + Task { @MainActor in + let result = await localAuth.authorizeUser(reason: reason) switch result { case .success: - self?.dialogService.showSuccess(withMessage: String.adamant.alert.done) - self?.accountService.updateUseBiometry(enabled) + self.dialogService.showSuccess(withMessage: String.adamant.alert.done) + self.accountService.updateUseBiometry(enabled) case .cancel: DispatchQueue.main.async { [weak self] in @@ -59,209 +60,201 @@ extension SecurityViewController { } case .fallback: - let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) + let pinpad = adamantPinpad(biometryButton: .none) { _ in } if enabled { - pinpad.commentLabel.text = String.adamant.security.biometryOnReason - self?.pinpadRequest = .turnOnBiometry +// pinpad.commentLabel.text = String.adamant.security.biometryOnReason + self.pinpadRequest = .turnOnBiometry } else { - pinpad.commentLabel.text = String.adamant.security.biometryOffReason - self?.pinpadRequest = .turnOffBiometry +// pinpad.commentLabel.text = String.adamant.security.biometryOffReason + self.pinpadRequest = .turnOffBiometry } - pinpad.commentLabel.isHidden = false - pinpad.delegate = self +// pinpad.commentLabel.isHidden = false +// pinpad.delegate = self - DispatchQueue.main.async { +// DispatchQueue.main.async { pinpad.modalPresentationStyle = .overFullScreen - self?.present(pinpad, animated: true, completion: nil) - } +// self.present(pinpad, animated: true, completion: nil) +// } case .failed: DispatchQueue.main.async { - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - if let value = self?.accountService.useBiometry { - row.value = value - } else { - row.value = false - } - + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = self.accountService.useBiometry row.updateCell() row.evaluateHidden() } - if let section = self?.form.sectionBy(tag: Sections.notifications.tag) { + if let section = self.form.sectionBy(tag: Sections.notifications.tag) { section.evaluateHidden() } } + case .biometryLockout: + break } } } } // MARK: - PinpadViewControllerDelegate -extension SecurityViewController: PinpadViewControllerDelegate { - nonisolated func pinpad(_ pinpad: PinpadViewController, didEnterPin pin: String) { - MainActor.assumeIsolatedSafe { - switch pinpadRequest { - - // MARK: User has entered new pin first time. Request re-enter pin - case .createPin?: - pinpadRequest = .reenterPin(pin: pin) - pinpad.commentLabel.text = String.adamant.pinpad.reenterPin - pinpad.clearPin() - return - - // MARK: User has reentered pin. Save pin. - case .reenterPin(let pinToVerify)?: - guard pin == pinToVerify else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.setStayLoggedIn(pin: pin) { [weak self] result in - Task { @MainActor in - switch result { - case .success: - self?.pinpadRequest = nil - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let section = self?.form.sectionBy(tag: Sections.notifications.tag) { - section.evaluateHidden() - } - - if let section = self?.form.sectionBy(tag: Sections.aboutNotificationTypes.tag) { - section.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) - - case .failure(let error): - self?.dialogService.showRichError(error: error) - } - } - } - - // MARK: Users want to turn off the pin. Validate and turn off. - case .turnOffPin?: - guard accountService.validatePin(pin) else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.dropSavedAccount() - - pinpad.dismiss(animated: true, completion: nil) - - // MARK: User wants to turn on biometry - case .turnOnBiometry?: - guard accountService.validatePin(pin) else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.updateUseBiometry(true) - pinpad.dismiss(animated: true, completion: nil) - - // MARK: User wants to turn off biometry - case .turnOffBiometry?: - guard accountService.validatePin(pin) else { - pinpad.playWrongPinAnimation() - pinpad.clearPin() - break - } - - accountService.updateUseBiometry(false) - pinpad.dismiss(animated: true, completion: nil) - - default: - pinpad.dismiss(animated: true, completion: nil) - } - } - } - - nonisolated func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { - MainActor.assumeIsolatedSafe { - switch pinpadRequest { - - // MARK: User wants to turn of StayIn with his face. Or finger. - case .turnOffPin?: - localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff, completion: { [weak self] result in - switch result { - case .success: - self?.accountService.dropSavedAccount() - - DispatchQueue.main.async { - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let section = self?.form.sectionBy(tag: Sections.notifications.tag) { - section.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) - } - - case .cancel: break - case .fallback: break - case .failed: break - } - }) - - default: - return - } - } - } - - nonisolated func pinpadDidCancel(_ pinpad: PinpadViewController) { - MainActor.assumeIsolatedSafe { - switch pinpadRequest { - - // MARK: User canceled turning on StayIn - case .createPin?, .reenterPin(pin: _)?: - if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { - row.value = false - row.updateCell() - } - - // MARK: User canceled turning off StayIn - case .turnOffPin?: - if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { - row.value = true - row.updateCell() - } - - // MARK: User canceled Biometry On - case .turnOnBiometry?: - if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - } - - // MARK: User canceled Biometry Off - case .turnOffBiometry?: - if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { - row.value = true - row.updateCell() - } - - default: - break - } - - pinpadRequest = nil - pinpad.dismiss(animated: true, completion: nil) - } - } -} +//extension SecurityViewController: PinpadViewControllerDelegate { +// nonisolated func pinpad(_ pinpad: PinpadViewController, didEnterPin pin: String) { +// MainActor.assumeIsolatedSafe { +// switch pinpadRequest { +// +// // MARK: User has entered new pin first time. Request re-enter pin +// case .createPin?: +// pinpadRequest = .reenterPin(pin: pin) +// pinpad.commentLabel.text = String.adamant.pinpad.reenterPin +// pinpad.clearPin() +// return +// +// // MARK: User has reentered pin. Save pin. +// case .reenterPin(let pinToVerify)?: +// guard pin == pinToVerify else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// let result = accountService.setStayLoggedIn(pin: pin) +// Task { @MainActor in +// switch result { +// case .success: +// self.pinpadRequest = nil +// if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// row.evaluateHidden() +// } +// +// if let section = self.form.sectionBy(tag: Sections.notifications.tag) { +// section.evaluateHidden() +// } +// +// if let section = self.form.sectionBy(tag: Sections.aboutNotificationTypes.tag) { +// section.evaluateHidden() +// } +// +// pinpad.dismiss(animated: true, completion: nil) +// +// case .failure(let error): +// self.dialogService.showRichError(error: error) +// } +// } +// +// // MARK: Users want to turn off the pin. Validate and turn off. +// case .turnOffPin?: +// guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// accountService.dropSavedAccount() +// +// pinpad.dismiss(animated: true, completion: nil) +// +// // MARK: User wants to turn on biometry +// case .turnOnBiometry?: +// guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// accountService.updateUseBiometry(true) +// pinpad.dismiss(animated: true, completion: nil) +// +// // MARK: User wants to turn off biometry +// case .turnOffBiometry?: +// guard accountService.validatePin(pin) else { +// pinpad.playWrongPinAnimation() +// pinpad.clearPin() +// break +// } +// +// accountService.updateUseBiometry(false) +// pinpad.dismiss(animated: true, completion: nil) +// +// default: +// pinpad.dismiss(animated: true, completion: nil) +// } +// } +// } +// +// nonisolated func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { +// Task { @MainActor in +// switch pinpadRequest { +// +// // MARK: User wants to turn of StayIn with his face. Or finger. +// case .turnOffPin?: +// let result = await localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff) +// switch result { +// case .success: +// self.accountService.dropSavedAccount() +// +// DispatchQueue.main.async { +// if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// row.evaluateHidden() +// } +// +// if let section = self.form.sectionBy(tag: Sections.notifications.tag) { +// section.evaluateHidden() +// } +// +// pinpad.dismiss(animated: true, completion: nil) +// } +// +// case .cancel, .fallback, .failed, .biometryLockout: break +// } +// default: +// break +// } +// } +// } +// +// nonisolated func pinpadDidCancel(_ pinpad: PinpadViewController) { +// MainActor.assumeIsolatedSafe { +// switch pinpadRequest { +// +// // MARK: User canceled turning on StayIn +// case .createPin?, .reenterPin(pin: _)?: +// if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { +// row.value = false +// row.updateCell() +// } +// +// // MARK: User canceled turning off StayIn +// case .turnOffPin?: +// if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { +// row.value = true +// row.updateCell() +// } +// +// // MARK: User canceled Biometry On +// case .turnOnBiometry?: +// if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { +// row.value = false +// row.updateCell() +// } +// +// // MARK: User canceled Biometry Off +// case .turnOffBiometry?: +// if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { +// row.value = true +// row.updateCell() +// } +// +// default: +// break +// } +// +// pinpadRequest = nil +// pinpad.dismiss(animated: true, completion: nil) +// } +// } +//} diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift index e8b4fbac7..c4db39344 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift @@ -120,7 +120,7 @@ extension AdmWalletService { [ Node.makeDefaultNode( url: URL(string: "https://info.adamant.im")!, - altUrl: URL(string: "http://88.198.156.44:44099")! + altUrl: URL(string: "http://5.161.98.136:33088")! ), Node.makeDefaultNode( url: URL(string: "https://info2.adm.im")!, diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift index 0f1f603cb..652e18048 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift @@ -71,8 +71,8 @@ extension BtcWalletService { 8 } - static let explorerTx = "https://explorer.btc.com/btc/transaction/" - static let explorerAddress = "https://explorer.btc.com/btc/address/" + static let explorerTx = "https://bitcoinexplorer.org/tx/" + static let explorerAddress = "https://bitcoinexplorer.org/address/" static var nodes: [Node] { [ diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift index 94be6f35e..8863452ba 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift @@ -266,6 +266,7 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { } } + @MainActor func update() async { guard let wallet = btcWallet else { return @@ -282,13 +283,12 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { setState(.updating) if let balance = try? await getBalance() { - markBalanceAsFresh() - if wallet.balance < balance, wallet.isBalanceInitialized { - await vibroService.applyVibration(.success) + vibroService.applyVibration(.success) } wallet.balance = balance + markBalanceAsFresh(wallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -363,12 +363,12 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { return output } - private func markBalanceAsFresh() { - btcWallet?.isBalanceInitialized = true + private func markBalanceAsFresh(_ wallet: BtcWallet) { + wallet.isBalanceInitialized = true balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) - guard let self, let wallet = btcWallet else { return } + guard let self else { return } wallet.isBalanceInitialized = false NotificationCenter.default.post( @@ -451,6 +451,7 @@ extension BtcWalletService { addressConverter: addressConverter ) self.btcWallet = eWallet + let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -467,9 +468,9 @@ extension BtcWalletService { let service = self do { let address = try await getWalletAddress(byAdamantAddress: adamant.address) - if address != eWallet.address { - service.save(btcAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(btcAddress: eWallet.address, result: result) + if address != eWallet.address, let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } throw WalletServiceError.accountNotFound } @@ -492,8 +493,10 @@ extension BtcWalletService { await service.update() } - service.save(btcAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(btcAddress: eWallet.address, result: result) + if let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) + } } return eWallet @@ -536,16 +539,11 @@ extension BtcWalletService { } func getBalance(address: String) async throws -> Decimal { - do { - let response: BtcBalanceResponse = try await btcApiService.request(waitsForConnectivity: false) { api, origin in - await api.sendRequestJsonResponse(origin: origin, path: BtcApiCommands.balance(for: address)) - }.get() - - return response.value / BtcWalletService.multiplier - } catch { - print("--debug", error.localizedDescription) - return 0 - } + let response: BtcBalanceResponse = try await btcApiService.request(waitsForConnectivity: false) { api, origin in + await api.sendRequestJsonResponse(origin: origin, path: BtcApiCommands.balance(for: address)) + }.get() + + return response.value / BtcWalletService.multiplier } func getFeeRate() async throws -> Decimal { @@ -568,8 +566,8 @@ extension BtcWalletService { /// - btcAddress: Bitcoin address to save into KVS /// - adamantAddress: Owner of BTC address /// - completion: success - private func save(btcAddress: String, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { - guard let adamant = accountService.account, let keypair = accountService.keypair else { + private func save(_ model: KVSValueModel, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { + guard let adamant = accountService.account else { completion(.failure(error: .notLogged)) return } @@ -580,13 +578,7 @@ extension BtcWalletService { } Task { - let result = await apiService.store( - key: BtcWalletService.kvsAddress, - value: btcAddress, - type: .keyValue, - sender: adamant.address, - keypair: keypair - ) + let result = await apiService.store(model) switch result { case .success: @@ -599,7 +591,7 @@ extension BtcWalletService { } /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save - private func kvsSaveCompletionRecursion(btcAddress: String, result: WalletServiceSimpleResult) { + private func kvsSaveCompletionRecursion(_ model: KVSValueModel, result: WalletServiceSimpleResult) { if let observer = balanceObserver { NotificationCenter.default.removeObserver(observer) balanceObserver = nil @@ -618,8 +610,8 @@ extension BtcWalletService { return } - self?.save(btcAddress: btcAddress) { [weak self] result in - self?.kvsSaveCompletionRecursion(btcAddress: btcAddress, result: result) + self?.save(model) { [weak self] result in + self?.kvsSaveCompletionRecursion(model, result: result) } } @@ -660,6 +652,16 @@ extension BtcWalletService { } } } + + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { + guard let keypair = accountService.keypair else { return nil } + + return .init( + key: Self.kvsAddress, + value: wallet.address, + keypair: keypair + ) + } } // MARK: - Transactions diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService.swift b/Adamant/Modules/Wallets/Dash/DashWalletService.swift index 646994f55..560992f08 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService.swift @@ -239,6 +239,7 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { } } + @MainActor func update() async { guard let wallet = dashWallet else { return @@ -255,13 +256,12 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { setState(.updating) if let balance = try? await getBalance() { - markBalanceAsFresh() - if wallet.balance < balance, wallet.isBalanceInitialized { - await vibroService.applyVibration(.success) + vibroService.applyVibration(.success) } wallet.balance = balance + markBalanceAsFresh(wallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -284,12 +284,12 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { } } - private func markBalanceAsFresh() { - dashWallet?.isBalanceInitialized = true + private func markBalanceAsFresh(_ wallet: DashWallet) { + wallet.isBalanceInitialized = true balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) - guard let self, let wallet = dashWallet else { return } + guard let self else { return } wallet.isBalanceInitialized = false NotificationCenter.default.post( @@ -331,6 +331,7 @@ extension DashWalletService { ) self.dashWallet = eWallet + let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -347,9 +348,9 @@ extension DashWalletService { do { let address = try await getWalletAddress(byAdamantAddress: adamant.address) let service = self - if address != eWallet.address { - service.save(dashAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(dashAddress: eWallet.address, result: result) + if address != eWallet.address, let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } @@ -371,9 +372,12 @@ extension DashWalletService { await service.update() } - service.save(dashAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(dashAddress: eWallet.address, result: result) + if let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) + } } + service.setState(.upToDate) return eWallet @@ -521,8 +525,8 @@ extension DashWalletService { /// - dashAddress: DASH address to save into KVS /// - adamantAddress: Owner of Dash address /// - completion: success - private func save(dashAddress: String, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { - guard let adamant = accountService.account, let keypair = accountService.keypair else { + private func save(_ model: KVSValueModel, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { + guard let adamant = accountService.account else { completion(.failure(error: .notLogged)) return } @@ -533,13 +537,7 @@ extension DashWalletService { } Task { @Sendable in - let result = await apiService.store( - key: DashWalletService.kvsAddress, - value: dashAddress, - type: .keyValue, - sender: adamant.address, - keypair: keypair - ) + let result = await apiService.store(model) switch result { case .success: @@ -552,7 +550,7 @@ extension DashWalletService { } /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save - private func kvsSaveCompletionRecursion(dashAddress: String, result: WalletServiceSimpleResult) { + private func kvsSaveCompletionRecursion(_ model: KVSValueModel, result: WalletServiceSimpleResult) { if let observer = balanceObserver { NotificationCenter.default.removeObserver(observer) balanceObserver = nil @@ -571,8 +569,8 @@ extension DashWalletService { return } - self?.save(dashAddress: dashAddress) { [weak self] result in - self?.kvsSaveCompletionRecursion(dashAddress: dashAddress, result: result) + self?.save(model) { [weak self] result in + self?.kvsSaveCompletionRecursion(model, result: result) } } @@ -584,6 +582,16 @@ extension DashWalletService { } } } + + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { + guard let keypair = accountService.keypair else { return nil } + + return .init( + key: Self.kvsAddress, + value: wallet.address, + keypair: keypair + ) + } } // MARK: - PrivateKey generator diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift index 99a9aebd9..480dda84c 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift @@ -236,6 +236,7 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { } } + @MainActor func update() async { guard let wallet = dogeWallet else { return @@ -252,13 +253,12 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { setState(.updating) if let balance = try? await getBalance() { - markBalanceAsFresh() - if wallet.balance < balance, wallet.isBalanceInitialized { - await vibroService.applyVibration(.success) + vibroService.applyVibration(.success) } wallet.balance = balance + markBalanceAsFresh(wallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -281,12 +281,12 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { } } - private func markBalanceAsFresh() { - dogeWallet?.isBalanceInitialized = true + private func markBalanceAsFresh(_ wallet: DogeWallet) { + wallet.isBalanceInitialized = true balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) - guard let self, let wallet = dogeWallet else { return } + guard let self else { return } wallet.isBalanceInitialized = false NotificationCenter.default.post( @@ -326,6 +326,7 @@ extension DogeWalletService { addressConverter: addressConverter ) self.dogeWallet = eWallet + let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -342,9 +343,9 @@ extension DogeWalletService { let service = self do { let address = try await getWalletAddress(byAdamantAddress: adamant.address) - if address != eWallet.address { - service.save(dogeAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(dogeAddress: eWallet.address, result: result) + if address != eWallet.address, let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } @@ -366,9 +367,12 @@ extension DogeWalletService { await service.update() } - service.save(dogeAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(dogeAddress: eWallet.address, result: result) + if let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) + } } + service.setState(.upToDate) return eWallet @@ -455,8 +459,8 @@ extension DogeWalletService { /// - dogeAddress: DOGE address to save into KVS /// - adamantAddress: Owner of Doge address /// - completion: success - private func save(dogeAddress: String, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { - guard let adamant = accountService.account, let keypair = accountService.keypair else { + private func save(_ model: KVSValueModel, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { + guard let adamant = accountService.account else { completion(.failure(error: .notLogged)) return } @@ -467,13 +471,7 @@ extension DogeWalletService { } Task { @Sendable in - let result = await apiService.store( - key: DogeWalletService.kvsAddress, - value: dogeAddress, - type: .keyValue, - sender: adamant.address, - keypair: keypair - ) + let result = await apiService.store(model) switch result { case .success: @@ -486,7 +484,7 @@ extension DogeWalletService { } /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save - private func kvsSaveCompletionRecursion(dogeAddress: String, result: WalletServiceSimpleResult) { + private func kvsSaveCompletionRecursion(_ model: KVSValueModel, result: WalletServiceSimpleResult) { if let observer = balanceObserver { NotificationCenter.default.removeObserver(observer) balanceObserver = nil @@ -505,8 +503,8 @@ extension DogeWalletService { return } - self?.save(dogeAddress: dogeAddress) { [weak self] result in - self?.kvsSaveCompletionRecursion(dogeAddress: dogeAddress, result: result) + self?.save(model) { [weak self] result in + self?.kvsSaveCompletionRecursion(model, result: result) } } @@ -518,6 +516,16 @@ extension DogeWalletService { } } } + + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { + guard let keypair = accountService.keypair else { return nil } + + return .init( + key: Self.kvsAddress, + value: wallet.address, + keypair: keypair + ) + } } // MARK: - Transactions diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift index 6a78f4e92..7c4a86c1a 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift @@ -208,13 +208,6 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) - NotificationCenter.default - .notifications(named: .AdamantAccountService.accountDataUpdated, object: nil) - .sink { @MainActor [weak self] _ in - self?.update() - } - .store(in: &subscriptions) - NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in @@ -245,6 +238,7 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { } } + @MainActor func update() async { guard let wallet = ethWallet else { return @@ -261,13 +255,12 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { setState(.updating) if let balance = try? await getBalance(forAddress: wallet.ethAddress) { - markBalanceAsFresh() - if wallet.balance < balance, wallet.isBalanceInitialized { - await vibroService.applyVibration(.success) + vibroService.applyVibration(.success) } wallet.balance = balance + markBalanceAsFresh(wallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -348,12 +341,12 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { }.get() } - private func markBalanceAsFresh() { - ethWallet?.isBalanceInitialized = true + private func markBalanceAsFresh(_ wallet: EthWallet) { + wallet.isBalanceInitialized = true balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) - guard let self, let wallet = ethWallet else { return } + guard let self else { return } wallet.isBalanceInitialized = false NotificationCenter.default.post( diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift index 9ab46ed99..aeda63dc5 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift @@ -291,13 +291,12 @@ final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { setState(.updating) if let balance = try? await getBalance(forAddress: wallet.ethAddress) { - markBalanceAsFresh() - if wallet.balance < balance, wallet.isBalanceInitialized { vibroService.applyVibration(.success) } wallet.balance = balance + markBalanceAsFresh(wallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -310,12 +309,12 @@ final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { await calculateFee() } - private func markBalanceAsFresh() { - ethWallet?.isBalanceInitialized = true + private func markBalanceAsFresh(_ wallet: EthWallet) { + wallet.isBalanceInitialized = true balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) - guard let self, let wallet = ethWallet else { return } + guard let self else { return } wallet.isBalanceInitialized = false NotificationCenter.default.post( @@ -429,6 +428,7 @@ extension EthWalletService { // MARK: 3. Update ethWallet = eWallet + let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -445,9 +445,9 @@ extension EthWalletService { let service = self do { let address = try await getWalletAddress(byAdamantAddress: adamant.address) - if eWallet.address.caseInsensitiveCompare(address) != .orderedSame { - service.save(ethAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(ethAddress: eWallet.address.lowercased(), result: result) + if eWallet.address.caseInsensitiveCompare(address) != .orderedSame, let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } @@ -469,8 +469,10 @@ extension EthWalletService { await service.update() } - service.save(ethAddress: eWallet.address) { result in - service.kvsSaveCompletionRecursion(ethAddress: eWallet.address, result: result) + if let kvsAddressModel { + service.save(kvsAddressModel) { result in + service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) + } } return eWallet @@ -488,7 +490,7 @@ extension EthWalletService { } /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save - private func kvsSaveCompletionRecursion(ethAddress: String, result: WalletServiceSimpleResult) { + private func kvsSaveCompletionRecursion(_ model: KVSValueModel, result: WalletServiceSimpleResult) { if let observer = balanceObserver { NotificationCenter.default.removeObserver(observer) balanceObserver = nil @@ -507,8 +509,8 @@ extension EthWalletService { return } - self?.save(ethAddress: ethAddress) { [weak self] result in - self?.kvsSaveCompletionRecursion(ethAddress: ethAddress, result: result) + self?.save(model) { [weak self] result in + self?.kvsSaveCompletionRecursion(model, result: result) } } @@ -585,8 +587,8 @@ extension EthWalletService { /// - ethAddress: Ethereum address to save into KVS /// - adamantAddress: Owner of Ethereum address /// - completion: success - private func save(ethAddress: String, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { - guard let adamant = accountService?.account, let keypair = accountService?.keypair else { + private func save(_ model: KVSValueModel, completion: @escaping @Sendable (WalletServiceSimpleResult) -> Void) { + guard let adamant = accountService?.account else { completion(.failure(error: .notLogged)) return } @@ -597,13 +599,7 @@ extension EthWalletService { } Task { - let result = await apiService.store( - key: EthWalletService.kvsAddress, - value: ethAddress, - type: .keyValue, - sender: adamant.address, - keypair: keypair - ) + let result = await apiService.store(model) switch result { case .success: @@ -614,6 +610,16 @@ extension EthWalletService { } } } + + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { + guard let keypair = accountService?.keypair else { return nil } + + return .init( + key: Self.kvsAddress, + value: wallet.address.lowercased(), + keypair: keypair + ) + } } // MARK: - Transactions diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift index 8bc1e616e..045164a30 100644 --- a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift @@ -229,6 +229,7 @@ private extension KlyWalletService { .store(in: &subscriptions) } + @MainActor func update() async { guard let wallet = klyWallet else { return @@ -245,13 +246,12 @@ private extension KlyWalletService { setState(.updating) if let balance = try? await getBalance() { - markBalanceAsFresh() - if wallet.balance < balance, wallet.isBalanceInitialized { - await vibroService.applyVibration(.success) + vibroService.applyVibration(.success) } wallet.balance = balance + markBalanceAsFresh(wallet) NotificationCenter.default.post( name: walletUpdatedNotification, @@ -275,12 +275,12 @@ private extension KlyWalletService { setState(.upToDate) } - func markBalanceAsFresh() { - klyWallet?.isBalanceInitialized = true + func markBalanceAsFresh(_ wallet: KlyWallet) { + wallet.isBalanceInitialized = true balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) - guard let self, let wallet = klyWallet else { return } + guard let self else { return } wallet.isBalanceInitialized = false NotificationCenter.default.post( @@ -442,7 +442,10 @@ private extension KlyWalletService { NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - guard let eWallet = self.klyWallet else { + guard + let eWallet = klyWallet, + let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) + else { throw WalletServiceError.accountNotFound } @@ -452,7 +455,7 @@ private extension KlyWalletService { let address = try await getWalletAddress(byAdamantAddress: adamant.address) if address != eWallet.address { - updateKvsAddress(eWallet.address) + updateKvsAddress(kvsAddressModel) } setState(.upToDate) @@ -473,7 +476,7 @@ private extension KlyWalletService { await update() } - updateKvsAddress(eWallet.address) + updateKvsAddress(kvsAddressModel) return eWallet default: @@ -483,13 +486,13 @@ private extension KlyWalletService { } } - func updateKvsAddress(_ address: String) { + func updateKvsAddress(_ model: KVSValueModel) { Task { do { - try await save(klyAddress: address) + try await save(model) } catch { kvsSaveProcessError( - klyAddress: address, + model, error: error ) } @@ -498,7 +501,7 @@ private extension KlyWalletService { /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save func kvsSaveProcessError( - klyAddress: String, + _ model: KVSValueModel, error: Error ) { guard let error = error as? WalletServiceError, @@ -517,7 +520,7 @@ private extension KlyWalletService { guard let self = self else { return } Task { - try await self.save(klyAddress: klyAddress) + try await self.save(model) self.balanceObserver?.cancel() } } @@ -526,10 +529,8 @@ private extension KlyWalletService { /// - Parameters: /// - klyAddress: Klayr address to save into KVS /// - adamantAddress: Owner of Klayr address - func save(klyAddress: String) async throws { - guard let adamant = accountService.account, - let keypair = accountService.keypair - else { + func save(_ model: KVSValueModel) async throws { + guard let adamant = accountService.account else { throw WalletServiceError.notLogged } @@ -537,13 +538,7 @@ private extension KlyWalletService { throw WalletServiceError.notEnoughMoney } - let result = await apiService.store( - key: KlyWalletService.kvsAddress, - value: klyAddress, - type: .keyValue, - sender: adamant.address, - keypair: keypair - ) + let result = await apiService.store(model) guard case .failure(let error) = result else { return @@ -552,6 +547,16 @@ private extension KlyWalletService { throw WalletServiceError.apiError(error) } + func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { + guard let keypair = accountService.keypair else { return nil } + + return .init( + key: Self.kvsAddress, + value: wallet.address, + keypair: keypair + ) + } + func getKlyWalletAddress( byAdamantAddress address: String ) async throws -> String { diff --git a/Adamant/Release.entitlements b/Adamant/Release.entitlements index 85b260349..10349a3bc 100644 --- a/Adamant/Release.entitlements +++ b/Adamant/Release.entitlements @@ -22,7 +22,7 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.device.camera @@ -32,7 +32,7 @@ keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger + $(AppIdentifierPrefix)im.adamant.messenger-random1818 diff --git a/Adamant/ServiceProtocols/AccountService.swift b/Adamant/ServiceProtocols/AccountService.swift index edf5c780c..06017c9ae 100644 --- a/Adamant/ServiceProtocols/AccountService.swift +++ b/Adamant/ServiceProtocols/AccountService.swift @@ -185,8 +185,8 @@ protocol AccountService: AnyObject, Sendable { /// /// - Parameters: /// - pin: pincode to login - /// - completion: completion handler - func setStayLoggedIn(pin: String, completion: @escaping @Sendable (AccountServiceResult) -> Void) + /// - Returns:AccountServiceResult with either success or failure + func setStayLoggedIn(pin: String) -> AccountServiceResult /// Remove stored data func dropSavedAccount() diff --git a/Adamant/ServiceProtocols/DataProviders/DataProvider.swift b/Adamant/ServiceProtocols/DataProviders/DataProvider.swift index 395453c24..1272b84f6 100644 --- a/Adamant/ServiceProtocols/DataProviders/DataProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/DataProvider.swift @@ -9,7 +9,7 @@ import Foundation import CommonKit -enum State { +enum DataProviderState { case empty case updating case upToDate @@ -17,8 +17,8 @@ enum State { } protocol DataProvider: AnyObject, Actor { - var state: State { get } - var stateObserver: AnyObservable { get } + var state: DataProviderState { get } + var stateObserver: AnyObservable { get } var isInitiallySynced: Bool { get } func reload() async @@ -26,10 +26,10 @@ protocol DataProvider: AnyObject, Actor { } // MARK: - Status Equatable -extension State: Equatable { +extension DataProviderState: Equatable { /// Simple equatable function. Does not checks associated values. - static func ==(lhs: State, rhs: State) -> Bool { + static func ==(lhs: DataProviderState, rhs: DataProviderState) -> Bool { switch (lhs, rhs) { case (.empty, .empty): return true case (.updating, .updating): return true diff --git a/Adamant/ServiceProtocols/LocalAuthentication.swift b/Adamant/ServiceProtocols/LocalAuthentication.swift index 11f32ea2a..c415b0bce 100644 --- a/Adamant/ServiceProtocols/LocalAuthentication.swift +++ b/Adamant/ServiceProtocols/LocalAuthentication.swift @@ -22,6 +22,7 @@ enum BiometryType { enum AuthenticationResult { case success + case biometryLockout case cancel case fallback case failed @@ -30,5 +31,5 @@ enum AuthenticationResult { protocol LocalAuthentication: AnyObject { var biometryType: BiometryType { get } - func authorizeUser(reason: String, completion: @escaping (AuthenticationResult) -> Void) + func authorizeUser(reason: String) async -> AuthenticationResult } diff --git a/Adamant/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index 36872bdb3..b827ef844 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -89,15 +89,13 @@ final class AdamantAccountService: AccountService, @unchecked Sendable { // MARK: - Saved data extension AdamantAccountService { - func setStayLoggedIn(pin: String, completion: @escaping @Sendable (AccountServiceResult) -> Void) { + func setStayLoggedIn(pin: String) -> AccountServiceResult { guard let account = account, let keypair = keypair else { - completion(.failure(.userNotLogged)) - return + return .failure(.userNotLogged) } if hasStayInAccount { - completion(.failure(.internalError(message: "Already has account", error: nil))) - return + return .failure(.internalError(message: "Already has account", error: nil)) } securedStore.set(pin, for: .pin) @@ -111,7 +109,7 @@ extension AdamantAccountService { hasStayInAccount = true NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.stayInChanged, object: self, userInfo: [AdamantUserInfoKey.AccountService.newStayInState : true]) - completion(.success(account: account, alert: nil)) + return .success(account: account, alert: nil) } func validatePin(_ pin: String) -> Bool { @@ -397,7 +395,7 @@ extension AdamantAccountService { self.state = .loggedIn return account - } catch let error as ApiServiceError { + } catch let error { self.state = .notLogged switch error { @@ -407,8 +405,6 @@ extension AdamantAccountService { default: throw AccountServiceError.apiError(error: error) } - } catch { - throw AccountServiceError.internalError(message: error.localizedDescription, error: error) } } @@ -427,10 +423,9 @@ extension AdamantAccountService { return await withTaskGroup(of: WalletAccount?.self) { group in for wallet in walletServiceCompose.getWallets() { group.addTask { - let result = try? await wallet.core.initWallet( + try? await wallet.core.initWallet( withPassphrase: passphrase ) - return result } } @@ -495,7 +490,7 @@ private extension SecuredStore { } func get(_ key: Key) -> String? { - return get(key.stringValue) + get(key.stringValue) } func remove(_ key: Key) { diff --git a/Adamant/Services/AdamantAddressBookService.swift b/Adamant/Services/AdamantAddressBookService.swift index 088d35be9..5b346af36 100644 --- a/Adamant/Services/AdamantAddressBookService.swift +++ b/Adamant/Services/AdamantAddressBookService.swift @@ -90,7 +90,7 @@ final class AdamantAddressBookService: AddressBookService { // MARK: - Observer Actions private func userWillLogOut() async { - guard hasChanges else { + guard hasChanges, let keypair = accountService.keypair else { return } @@ -99,7 +99,7 @@ final class AdamantAddressBookService: AddressBookService { self.savingBookOnLogoutTaskId = .invalid } - _ = try? await saveAddressBook(self.addressBook) + _ = try? await saveAddressBook(self.addressBook, keypair: keypair) UIApplication.shared.endBackgroundTask(savingBookOnLogoutTaskId) savingBookOnLogoutTaskId = .invalid @@ -214,7 +214,7 @@ final class AdamantAddressBookService: AddressBookService { // MARK: - Saving func saveIfNeeded() async { - guard hasChanges else { + guard hasChanges, let keypair = accountService.keypair else { return } @@ -224,7 +224,7 @@ final class AdamantAddressBookService: AddressBookService { self.savingBookTaskId = .invalid } - guard let id = try? await saveAddressBook(addressBook) else { + guard let id = try? await saveAddressBook(addressBook, keypair: keypair) else { return } @@ -249,8 +249,8 @@ final class AdamantAddressBookService: AddressBookService { self.savingBookTaskId = .invalid } - private func saveAddressBook(_ book: [String: String]) async throws -> UInt64 { - guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { + private func saveAddressBook(_ book: [String: String], keypair: Keypair) async throws -> UInt64 { + guard let loggedAccount = accountService.account else { throw AddressBookServiceError.notLogged } @@ -279,13 +279,11 @@ final class AdamantAddressBookService: AddressBookService { // MARK: 2. Submit to KVS do { - let id = try await apiService.store( + let id = try await apiService.store(.init( key: addressBookKey, value: value, - type: .keyValue, - sender: address, keypair: keypair - ).get() + )).get() return id } catch let error { diff --git a/Adamant/Services/AdamantAuthentication.swift b/Adamant/Services/AdamantAuthentication.swift index d3aac41b2..38e92459c 100644 --- a/Adamant/Services/AdamantAuthentication.swift +++ b/Adamant/Services/AdamantAuthentication.swift @@ -15,21 +15,11 @@ final class AdamantAuthentication: LocalAuthentication { var error: NSError? let available: Bool - if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - available = true - } else if let errorCode = error?.code { - let lockoutCode = LAError.biometryLockout.rawValue - - if errorCode == lockoutCode { - available = true - } else { - available = false - } - } else { - available = false - } - - if available { + available = context.canEvaluatePolicy( + .deviceOwnerAuthenticationWithBiometrics, + error: &error + ) + if available || error?.code == LAError.biometryLockout.rawValue { switch context.biometryType { case .none, .opticID: return .none @@ -47,48 +37,47 @@ final class AdamantAuthentication: LocalAuthentication { } } - func authorizeUser(reason: String, completion: @escaping (AuthenticationResult) -> Void) { + func authorizeUser(reason: String) async -> AuthenticationResult { let context = LAContext() - context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { (success, error) in - if success { - completion(.success) - return - } - - guard let error = error as? LAError else { - completion(.failed) - return - } - - if error.code == LAError.userFallback { - completion(.fallback) - return - } - - if error.code == LAError.userCancel { - completion(.cancel) - return + let result = await authorizeUser( + context: context, + policy: .deviceOwnerAuthenticationWithBiometrics, + reason: reason + ) + if result == .biometryLockout { + return await authorizeUser( + context: context, + policy: .deviceOwnerAuthentication, + reason: reason + ) + } + return result + } + + private func authorizeUser( + context: LAContext, + policy: LAPolicy, + reason: String + ) async -> AuthenticationResult { + do { + let result = try await context.evaluatePolicy(policy, localizedReason: reason) + if result { + return .success } - - let tryDeviceOwner = error.code == LAError.biometryLockout - - if tryDeviceOwner { - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { (success, error) in - let result: AuthenticationResult - - if success { - result = .success - } else if let error = error as? LAError, error.code == LAError.userCancel { - result = .cancel - } else { - result = .failed - } - - completion(result) - } - } else { - completion(.failed) + } catch let error as LAError { + switch error.code { + case .userFallback: + return .fallback + case .biometryLockout: + return .biometryLockout + case .userCancel: + return .cancel + default: + return .failed } + } catch { + return .failed } + return .failed } } diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index e75b27790..c2b78f1de 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -28,10 +28,10 @@ actor AdamantChatsProvider: ChatsProvider { let stack: CoreDataStack // MARK: Properties - @ObservableValue private var stateNotifier: State = .empty - var stateObserver: AnyObservable { $stateNotifier.eraseToAnyPublisher() } + @ObservableValue private var stateNotifier: DataProviderState = .empty + var stateObserver: AnyObservable { $stateNotifier.eraseToAnyPublisher() } - private(set) var state: State = .empty + private(set) var state: DataProviderState = .empty private(set) var receivedLastHeight: Int64? private(set) var readedLastHeight: Int64? private let apiTransactions = 100 @@ -228,7 +228,7 @@ actor AdamantChatsProvider: ChatsProvider { // MARK: Tools /// Free stateSemaphore before calling this method, or you will deadlock. - private func setState(_ state: State, previous prevState: State, notify: Bool = true) { + private func setState(_ state: DataProviderState, previous prevState: DataProviderState, notify: Bool = true) { self.state = state guard notify else { return } diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index c64361867..158ef01fb 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -25,8 +25,8 @@ actor AdamantTransfersProvider: TransfersProvider { private let transactionService: ChatTransactionService weak var chatsProvider: ChatsProvider? - @ObservableValue private(set) var state: State = .empty - var stateObserver: AnyObservable { $state.eraseToAnyPublisher() } + @ObservableValue private(set) var state: DataProviderState = .empty + var stateObserver: AnyObservable { $state.eraseToAnyPublisher() } private(set) var isInitiallySynced: Bool = false private(set) var receivedLastHeight: Int64? private(set) var readedLastHeight: Int64? @@ -41,7 +41,7 @@ actor AdamantTransfersProvider: TransfersProvider { // MARK: Tools /// Free stateSemaphore before calling this method, or you will deadlock. - private func setState(_ state: State, previous prevState: State, notify: Bool = false) { + private func setState(_ state: DataProviderState, previous prevState: DataProviderState, notify: Bool = false) { self.state = state if notify { diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/Contents.json new file mode 100644 index 000000000..092b5c8bf --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "clipboard.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "clipboard@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "clipboard@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard.png new file mode 100644 index 000000000..a95528e0c Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard@2x.png new file mode 100644 index 000000000..7ec9d3415 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard@2x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard@3x.png new file mode 100644 index 000000000..fbf0a9592 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Models/KVSValueModel.swift b/CommonKit/Sources/CommonKit/Models/KVSValueModel.swift new file mode 100644 index 000000000..481dd2af5 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/KVSValueModel.swift @@ -0,0 +1,22 @@ +// +// KVSValueModel.swift +// CommonKit +// +// Created by Andrew G on 15.01.2025. +// + +public struct KVSValueModel: Sendable { + public let key: String + public let value: String + public let keypair: Keypair + + public init( + key: String, + value: String, + keypair: Keypair + ) { + self.key = key + self.value = value + self.keypair = keypair + } +} diff --git a/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift index a77ea7bc1..d87919f9f 100644 --- a/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift @@ -74,13 +74,7 @@ public protocol AdamantApiServiceProtocol: ApiServiceProtocol { // MARK: - States /// - Returns: Transaction ID - func store( - key: String, - value: String, - type: StateType, - sender: String, - keypair: Keypair - ) async -> ApiServiceResult + func store(_ model: KVSValueModel) async -> ApiServiceResult func get( key: String, diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift index 705d51a1d..d68340d7d 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift @@ -20,27 +20,27 @@ public extension ApiCommands { extension AdamantApiService { public static let KvsFee: Decimal = 0.001 - public func store( - key: String, - value: String, - type: StateType, - sender: String, - keypair: Keypair - ) async -> ApiServiceResult { + public func store(_ model: KVSValueModel) async -> ApiServiceResult { let transaction = NormalizedTransaction( type: .state, amount: .zero, - senderPublicKey: keypair.publicKey, + senderPublicKey: model.keypair.publicKey, requesterPublicKey: nil, date: .now, recipientId: nil, - asset: TransactionAsset(state: StateAsset(key: key, value: value, type: .keyValue)) + asset: TransactionAsset(state: StateAsset( + key: model.key, + value: model.value, + type: .keyValue + )) ) guard let transaction = adamantCore.makeSignedTransaction( transaction: transaction, - senderId: sender, - keypair: keypair + senderId: AdamantUtilities.generateAddress( + publicKey: model.keypair.publicKey + ), + keypair: model.keypair ) else { return .failure(.internalError(error: InternalAPIError.signTransactionFailed)) } diff --git a/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift index 10833013f..cea132917 100644 --- a/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift @@ -48,28 +48,26 @@ public final class BlockchainHealthCheckWrapper< } } - public override func healthCheckInternal() { - super.healthCheckInternal() + public override func healthCheckInternal() async { + await super.healthCheckInternal() + updateNodesAvailability(update: nil) - Task { @HealthCheckActor in - updateNodesAvailability(update: nil) - - await withTaskGroup(of: Void.self, returning: Void.self) { group in - nodes.filter { $0.isEnabled }.forEach { node in - group.addTask { @HealthCheckActor [weak self] in - guard let self, !currentRequests.contains(node.id) else { return } - - currentRequests.insert(node.id) - defer { currentRequests.remove(node.id) } - - let update = await updateNodeStatusInfo(node: node) - updateNodesAvailability(update: update) - } + try? await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in + nodes.filter { $0.isEnabled }.forEach { node in + group.addTask { @HealthCheckActor [weak self] in + guard let self, !currentRequests.contains(node.id) else { return } + + currentRequests.insert(node.id) + defer { currentRequests.remove(node.id) } + + let update = await updateNodeStatusInfo(node: node) + try Task.checkCancellation() + updateNodesAvailability(update: update) } - - await group.waitForAll() - healthCheckPostProcessing() } + + try await group.waitForAll() + healthCheckPostProcessing() } } } diff --git a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift index 7835fc2f6..fefbffad6 100644 --- a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift @@ -45,7 +45,9 @@ open class HealthCheckWrapper: S private let crucialUpdateInterval: TimeInterval private let isActive: Bool private var healthCheckTimerSubscription: AnyCancellable? - private var previousAppState: UIApplication.State? + private var healthCheckSubscriptions = Set() + private var appState: AppState = .active + private var performHealthCheckWhenBecomeActive = false private var lastUpdateTime: Date? public nonisolated init( @@ -112,17 +114,38 @@ open class HealthCheckWrapper: S nonisolated public func healthCheck() { Task { @HealthCheckActor in - guard isActive else { return } + guard canPerformHealthCheck else { return } lastUpdateTime = .now updateHealthCheckTimerSubscription() - healthCheckInternal() + + Task { + await healthCheckInternal() + guard Task.isCancelled else { return } + performHealthCheckWhenBecomeActive = true + }.store(in: &healthCheckSubscriptions) } } - open func healthCheckInternal() {} + open func healthCheckInternal() async {} } private extension HealthCheckWrapper { + private enum AppState { + case active + case background + } + + var canPerformHealthCheck: Bool { + guard isActive else { return false } + + switch appState { + case .active: + return true + case .background: + return false + } + } + func configure(nodes: AnyObservable<[Node]>, connection: AnyObservable) { let connection = connection .removeDuplicates() @@ -149,7 +172,7 @@ private extension HealthCheckWrapper { NotificationCenter.default .notifications(named: UIApplication.willResignActiveNotification, object: nil) - .sink { @HealthCheckActor [weak self] _ in self?.previousAppState = .background } + .sink { @HealthCheckActor [weak self] _ in self?.willResignActiveAction() } .store(in: &subscriptions) } @@ -172,17 +195,22 @@ private extension HealthCheckWrapper { } func didBecomeActiveAction() { - defer { previousAppState = .active } + guard appState != .active else { return } + appState = .active - guard - previousAppState == .background, - let timeToUpdate = lastUpdateTime?.addingTimeInterval(normalUpdateInterval / 3), - Date.now > timeToUpdate - else { return } + let timeToUpdate = lastUpdateTime?.addingTimeInterval(normalUpdateInterval / 3) + ?? .adamantNullDate + guard performHealthCheckWhenBecomeActive || Date.now >= timeToUpdate else { return } + performHealthCheckWhenBecomeActive = false healthCheck() } + func willResignActiveAction() { + appState = .background + healthCheckSubscriptions = .init() + } + func updateNodes(_ newNodes: [Node]) { nodes = newNodes updateSortedNodes() diff --git a/MessageNotificationContentExtension/Debug.entitlements b/MessageNotificationContentExtension/Debug.entitlements index 630574fd2..64a8011fc 100644 --- a/MessageNotificationContentExtension/Debug.entitlements +++ b/MessageNotificationContentExtension/Debug.entitlements @@ -6,13 +6,13 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger-dev + $(AppIdentifierPrefix)im.adamant.messenger-dev-random1818 diff --git a/MessageNotificationContentExtension/Release.entitlements b/MessageNotificationContentExtension/Release.entitlements index 125fd8842..d9e9e0a25 100644 --- a/MessageNotificationContentExtension/Release.entitlements +++ b/MessageNotificationContentExtension/Release.entitlements @@ -6,13 +6,13 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger + $(AppIdentifierPrefix)im.adamant.messenger-random1818 diff --git a/NotificationServiceExtension/Debug.entitlements b/NotificationServiceExtension/Debug.entitlements index 630574fd2..64a8011fc 100644 --- a/NotificationServiceExtension/Debug.entitlements +++ b/NotificationServiceExtension/Debug.entitlements @@ -6,13 +6,13 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger-dev + $(AppIdentifierPrefix)im.adamant.messenger-dev-random1818 diff --git a/NotificationServiceExtension/Release.entitlements b/NotificationServiceExtension/Release.entitlements index 125fd8842..d9e9e0a25 100644 --- a/NotificationServiceExtension/Release.entitlements +++ b/NotificationServiceExtension/Release.entitlements @@ -6,13 +6,13 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger + $(AppIdentifierPrefix)im.adamant.messenger-random1818 diff --git a/Podfile.lock b/Podfile.lock index 4e31a8ce2..2bf7bda9d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -21,4 +21,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a30619b79caa4b5a7497b0600d449f34b5620eec -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift b/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift index 4a0e67b8c..2f625f49d 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift @@ -49,4 +49,4 @@ private extension AutoDismissManager { } } -private let autoDismissTimeInterval: TimeInterval = 4 +private let autoDismissTimeInterval: TimeInterval = 3 diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift index 75aeefe07..2bb645388 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift @@ -13,95 +13,103 @@ struct NotificationPresenterView: View { case vertical case horizontal } - - @State private var verticalDragTranslation: CGFloat = .zero - @State private var horizontalDragTranslation: CGFloat = .zero - @State private var minTranslationYForDismiss: CGFloat = .infinity - @State private var minTranslationXForDismiss: CGFloat = .infinity @State private var isTextLimited: Bool = true @State private var dismissEdge: Edge = .top - @State private var dragDirection: DragDirection? + @State private var dragDirection: DragDirection? + @State private var dynamicHeight: CGFloat = 0 + @State private var notificationHeight: CGFloat = 0 + @State private var offset: CGSize = .zero let model: NotificationModel let safeAreaInsets: EdgeInsets let dismissAction: () -> Void var body: some View { - NotificationView( - isTextLimited: $isTextLimited, - model: model - ) - .padding([.leading, .trailing], 10) - .padding([.top, .bottom], 10) + VStack { + NotificationView( + isTextLimited: $isTextLimited, + model: model + ) + .padding(10) + .padding([.top, .bottom], 10) + .background( + GeometryReader { geometry in + Color.clear + .onAppear { + notificationHeight = geometry.size.height + print("onappier") + } + .onChange(of: geometry.size.height) { newValue in + notificationHeight = newValue + } + } + ) + } + .frame(minHeight: notificationHeight + dynamicHeight) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.init(uiColor:.adamant.chatInputBarBorderColor), lineWidth: 1) ) .background(GeometryReader(content: processGeometry)) - .expanded(axes: .horizontal) - .offset(y: verticalDragTranslation < .zero ? verticalDragTranslation : .zero) - .offset(x: horizontalDragTranslation < .zero ? horizontalDragTranslation : .zero) - .gesture(dragGesture) .onTapGesture(perform: onTap) + .gesture(dragGesture) .cornerRadius(10) .padding(.horizontal, 15) .padding(.top, safeAreaInsets.top) + .offset(offset) + .animation(.interactiveSpring(), value: offset) .transition(.move(edge: dismissEdge)) } } - +private extension NotificationPresenterView { + func processGeometry(_ geometry: GeometryProxy) -> some View { + return Color.init(uiColor: .adamant.swipeBlockColor) + .cornerRadius(10) + } + func onTap() { + model.tapHandler?.value() + dismissAction() + dismissEdge = .top + } +} private extension NotificationPresenterView { var dragGesture: some Gesture { DragGesture() - .onChanged { - if dragDirection == nil { - dragDirection = abs($0.translation.height) > abs($0.translation.width) ? .vertical : .horizontal + .onChanged { value in + if dragDirection == nil || (abs(value.translation.width) <= 5 && abs(value.translation.height) <= 5) { + detectDragDirection(value: value) } - switch dragDirection { - case .vertical: - verticalDragTranslation = $0.translation.height - case .horizontal: - horizontalDragTranslation = $0.translation.width - case .none: - break + if dragDirection == .vertical && isTextLimited { + dynamicHeight = max(0, min(value.translation.height, 30)) + } + if dragDirection == .vertical, value.translation.height < 0 { + offset = CGSize(width: 0, height: value.translation.height) + } else if dragDirection == .horizontal, value.translation.width < 0 { + offset = CGSize(width: value.translation.width, height: 0) } } - .onEnded { - if $0.velocity.height < -100 || -$0.translation.height > minTranslationYForDismiss { - dismissEdge = .top - Task { dismissAction() } - } else if $0.velocity.width < -100 || $0.translation.width > minTranslationXForDismiss { - dismissEdge = .leading - Task { dismissAction() } - } else if $0.velocity.height > -100 || -$0.translation.height < minTranslationYForDismiss { - withAnimation { - horizontalDragTranslation = .zero + .onEnded { value in + if dragDirection == .vertical { + if value.translation.height > 25 { + model.cancelAutoDismiss?.value() isTextLimited = false + } else if value.translation.height < -30 { + Task { dismissAction() } } - model.cancelAutoDismiss?.value() - } else { - withAnimation { - verticalDragTranslation = .zero - horizontalDragTranslation = .zero + } else if dragDirection == .horizontal { + if value.translation.width < -100 { + dismissEdge = .leading + Task { dismissAction() } } } dragDirection = nil + dynamicHeight = 0 + offset = .zero } } - func processGeometry(_ geometry: GeometryProxy) -> some View { - DispatchQueue.main.async { - minTranslationYForDismiss = geometry.size.height / 2 - minTranslationXForDismiss = geometry.size.width / 2 - } - - return Color.init(uiColor: .adamant.swipeBlockColor) - .cornerRadius(10) - } - - func onTap() { - model.tapHandler?.value() - dismissAction() - dismissEdge = .top + func detectDragDirection(value: DragGesture.Value) { + let horizontalDistance = abs(value.translation.width), verticalDistance = abs(value.translation.height) + dragDirection = verticalDistance > horizontalDistance ? .vertical : .horizontal } } diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift index d30848285..bf2515df2 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift @@ -1,6 +1,6 @@ // // NotificationView.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // @@ -21,7 +21,6 @@ struct NotificationView: View { textStack Spacer(minLength: .zero) } - Image(systemName: isTextLimited ? pullDownIcon : pullUpIcon) .font(.title) .foregroundColor(.gray) @@ -50,7 +49,6 @@ private extension NotificationView { Text(description) .font(.system(size: 13)) .lineLimit(isTextLimited ? 3 : nil) - } } } diff --git a/TransferNotificationContentExtension/Debug.entitlements b/TransferNotificationContentExtension/Debug.entitlements index 630574fd2..64a8011fc 100644 --- a/TransferNotificationContentExtension/Debug.entitlements +++ b/TransferNotificationContentExtension/Debug.entitlements @@ -6,13 +6,13 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger-dev + $(AppIdentifierPrefix)im.adamant.messenger-dev-random1818 diff --git a/TransferNotificationContentExtension/Release.entitlements b/TransferNotificationContentExtension/Release.entitlements index 125fd8842..d9e9e0a25 100644 --- a/TransferNotificationContentExtension/Release.entitlements +++ b/TransferNotificationContentExtension/Release.entitlements @@ -6,13 +6,13 @@ com.apple.security.application-groups - group.adamant.adamant-messenger + group.adamant.adamant-messenger-random1818 com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)im.adamant.messenger + $(AppIdentifierPrefix)im.adamant.messenger-random1818