From f25f953cc561abfdcb709d86de8928524edd9503 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Tue, 25 Jul 2023 18:56:17 +0300 Subject: [PATCH] [PWN-9259] Finish confirm button action handlers --- .../BankTransfer/Models/UserWallet.swift | 13 +- .../Mock/MockStrigaRemoteProvider.swift | 5 + .../Striga/Models/StrigaEndpoint.swift | 26 ++- .../StrigaInitiateSEPAPaymentResponse.swift | 6 + .../Striga/Models/StrigaWithdrawalInfo.swift | 2 +- .../DataProviders/StrigaLocalProvider.swift | 2 +- .../DataProviders/StrigaRemoteProvider.swift | 9 + .../StrigaRemoteProviderImpl.swift | 6 + ...StrigaBankTransferUserDataRepository.swift | 6 + ...nkTransferOutgoingUserActionConsumer.swift | 155 ++++++++++++++++++ ...StrigaBankTransferUserActionConsumer.swift | 39 ----- .../StrigaRemoteProviderTests.swift | 23 ++- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Resolver+registerAllServices.swift | 4 + .../Resources/Base.lproj/Localizable.strings | 2 +- .../Resources/en.lproj/Localizable.strings | 2 +- .../Resources/fr.lproj/Localizable.strings | 2 +- .../Resources/ru.lproj/Localizable.strings | 2 +- .../Resources/vi.lproj/Localizable.strings | 2 +- .../Claim/StrigaClaimTransaction.swift | 2 + .../IBANDetails/IBANDetailsView.swift | 11 +- .../Scenes/Main/NewHome/HomeCoordinator.swift | 26 +-- .../AccountList/HomeAccountsView.swift | 8 +- .../AccountList/HomeAccountsViewModel.swift | 78 ++++++++- .../Model/BankTransferRenderableAccount.swift | 18 +- .../Subview/HomeBankTransferAccountView.swift | 1 - 26 files changed, 370 insertions(+), 84 deletions(-) create mode 100644 Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaInitiateSEPAPaymentResponse.swift create mode 100644 Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferOutgoingUserActionConsumer.swift diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Models/UserWallet.swift b/Packages/KeyAppKit/Sources/BankTransfer/Models/UserWallet.swift index e286f5d7e3..448f62709d 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Models/UserWallet.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Models/UserWallet.swift @@ -16,12 +16,23 @@ public struct EURUserAccount: Codable { public let iban: String? public let bic: String? public let bankAccountHolderName: String? + public let availableBalance: Int? - public init(accountID: String, currency: String, createdAt: String, enriched: Bool, iban: String? = nil, bic: String? = nil, bankAccountHolderName: String? = nil) { + public init( + accountID: String, + currency: String, + createdAt: String, + enriched: Bool, + availableBalance: Int?, + iban: String? = nil, + bic: String? = nil, + bankAccountHolderName: String? = nil + ) { self.accountID = accountID self.currency = currency self.createdAt = createdAt self.enriched = enriched + self.availableBalance = availableBalance self.iban = iban self.bic = bic self.bankAccountHolderName = bankAccountHolderName diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift index fd73258bac..2f2aeedb57 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift @@ -176,4 +176,9 @@ public final class MockStrigaRemoteProvider: StrigaRemoteProvider { StrigaGetAccountStatementResponse.Transaction(id: "a25e0dd1-8f4f-441d-a671-2f7d1e9738e6", txType: "SEPA_PAYIN_COMPLETED", bankingSenderBic: "BUKBGB22", bankingSenderIban: "GB29NWBK60161331926819") ]) } + + public func initiateSEPAPayment(userId: String, accountId: String, amount: String, iban: String, bic: String) async throws -> StrigaInitiateSEPAPaymentResponse { + fatalError() + } + } diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift index 420e5d8632..70f1a015e1 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift @@ -316,7 +316,31 @@ struct StrigaEndpoint: HTTPEndpoint { body: nil ) } - + + static func initiateSEPAPayment( + baseURL: String, + keyPair: KeyPair, + userId: String, + sourceAccountId: String, + amount: String, + iban: String, + bic: String + ) throws -> Self { + try StrigaEndpoint( + baseURL: baseURL, + path: "/wallets/send/initiate/bank", + method: .post, + keyPair: keyPair, + body: [ + "userId": .init(userId), + "sourceAccountId": .init(sourceAccountId), + "amount": .init(amount), + "destination": .init(["iban": .init(iban), + "bic": .init(bic)] as [String: KeyAppNetworking.AnyEncodable]), + ] as [String: KeyAppNetworking.AnyEncodable] + ) + } + static func getAccountStatement( baseURL: String, keyPair: KeyPair, diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaInitiateSEPAPaymentResponse.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaInitiateSEPAPaymentResponse.swift new file mode 100644 index 0000000000..50e5a41dab --- /dev/null +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaInitiateSEPAPaymentResponse.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct StrigaInitiateSEPAPaymentResponse: Codable { + let challengeId: String + let dateExpires: String +} diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaWithdrawalInfo.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaWithdrawalInfo.swift index c744582569..e127e6db19 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaWithdrawalInfo.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaWithdrawalInfo.swift @@ -1,6 +1,6 @@ import Foundation -public struct StrigaWithdrawalInfo: Codable { +public struct StrigaWithdrawalInfo: Codable, Equatable { public let IBAN: String? public let BIC: String? public let receiver: String diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaLocalProvider.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaLocalProvider.swift index 80e0314c3b..67f2a58e8f 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaLocalProvider.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaLocalProvider.swift @@ -32,7 +32,7 @@ public actor StrigaLocalProviderImpl { private func migrate() { // Migration - let migrationKey = "StrigaLocalProviderImpl.migration12" + let migrationKey = "StrigaLocalProviderImpl.migration13" if !UserDefaults.standard.bool(forKey: migrationKey) { clear() UserDefaults.standard.set(true, forKey: migrationKey) diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift index e3c79a7e05..2b65aa1232 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift @@ -100,4 +100,13 @@ public protocol StrigaRemoteProvider: AnyObject { /// - Parameter page: Page number /// - SeeAlso: [Get Account Statement](https://docs.striga.com/reference/get-account-statement) func getAccountStatement(userId: String, accountId: String, startDate: Date, endDate: Date, page: Int) async throws -> StrigaGetAccountStatementResponse + + /// Initiate SEPA Payment + /// - Parameter userId: The Id of the user who is sending this transaction + /// - Parameter accountId: The Id of the account to debit + /// - Parameter amount: The amount denominated in the smallest divisible unit of the sending currency. For example: cents + /// - Parameter iban: IBAN of the recipient - MUST be in the name of the account holder + /// - Parameter bic: BIC of the recipient - MUST be in the name of the account holder + /// - SeeAlso: [Initiate SEPA Payment](https://docs.striga.com/reference/initiate-sepa-payment) + func initiateSEPAPayment(userId: String, accountId: String, amount: String, iban: String, bic: String) async throws -> StrigaInitiateSEPAPaymentResponse } diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift index 4b5582cdea..194eef5cfc 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift @@ -268,6 +268,12 @@ extension StrigaRemoteProviderImpl: StrigaRemoteProvider { let endpoint = try StrigaEndpoint.getAccountStatement(baseURL: baseURL, keyPair: keyPair, userId: userId, accountId: accountId, startDate: startDate, endDate: endDate, page: page) return try await httpClient.request(endpoint: endpoint, responseModel: StrigaGetAccountStatementResponse.self) } + + public func initiateSEPAPayment(userId: String, accountId: String, amount: String, iban: String, bic: String) async throws -> StrigaInitiateSEPAPaymentResponse { + guard let keyPair else { throw BankTransferError.invalidKeyPair } + let endpoint = try StrigaEndpoint.initiateSEPAPayment(baseURL: baseURL, keyPair: keyPair, userId: userId, sourceAccountId: accountId, amount: amount, iban: iban, bic: bic) + return try await httpClient.request(endpoint: endpoint, responseModel: StrigaInitiateSEPAPaymentResponse.self) + } } // MARK: - Error response diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift index 93c64bd6bd..bea8e27db9 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift @@ -226,6 +226,7 @@ public final class StrigaBankTransferUserDataRepository: BankTransferUserDataRep currency: eur.currency, createdAt: eur.createdAt, enriched: true, + availableBalance: eur.availableBalance, iban: response.iban, bic: response.bic, bankAccountHolderName: response.bankAccountHolderName @@ -363,6 +364,10 @@ public final class StrigaBankTransferUserDataRepository: BankTransferUserDataRep throw StrigaProviderError.invalidRateTokens } + public func initiateSEPAPayment(userId: String, accountId: String, amount: String, iban: String, bic: String) async throws -> String { + try await remoteProvider.initiateSEPAPayment(userId: userId, accountId: accountId, amount: amount, iban: iban, bic: bic).challengeId + } + // MARK: - Private private func enrichAccount(userId: String, accountId: String) async throws -> T { try await remoteProvider.enrichAccount(userId: userId, accountId: accountId) @@ -399,6 +404,7 @@ private extension UserWallet { currency: eurAccount.currency, createdAt: eurAccount.createdAt, enriched: cached?.accounts.eur?.enriched ?? false, + availableBalance: Int(eurAccount.availableBalance.amount) ?? 0, iban: cached?.accounts.eur?.iban, bic: cached?.accounts.eur?.bic, bankAccountHolderName: cached?.accounts.eur?.bankAccountHolderName diff --git a/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferOutgoingUserActionConsumer.swift b/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferOutgoingUserActionConsumer.swift new file mode 100644 index 0000000000..e1fa4915d4 --- /dev/null +++ b/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferOutgoingUserActionConsumer.swift @@ -0,0 +1,155 @@ +import Combine +import Foundation +import KeyAppBusiness +import KeyAppKitCore +import SolanaSwift + +public enum OutgoingBankTransferUserActionResult: Codable, Equatable { + case requestWithdrawInfo(receiver: String) + case initiated(challengeId: String) +} + +public enum OutgoingBankTransferUserActionEvent: UserActionEvent { + case track(OutgoingBankTransferUserAction, UserActionStatus) + case complete(OutgoingBankTransferUserAction, OutgoingBankTransferUserActionResult) + case sendFailure(OutgoingBankTransferUserAction, String) +} + +public class StrigaBankTransferOutgoingUserActionConsumer: UserActionConsumer { + public typealias Action = OutgoingBankTransferUserAction + public typealias Event = OutgoingBankTransferUserActionEvent + + public let persistence: UserActionPersistentStorage + let database: SynchronizedDatabase = .init() + + private var bankTransferService: AnyBankTransferService + + public init( + persistence: UserActionPersistentStorage, + bankTransferService: AnyBankTransferService + ) { + self.persistence = persistence + self.bankTransferService = bankTransferService + } + + public var onUpdate: AnyPublisher { + database + .onUpdate + .flatMap { data in + Publishers.Sequence(sequence: Array(data.values)) + } + .eraseToAnyPublisher() + } + + public func start() {} + + public func process(action: any UserAction) { + guard let action = action as? Action else { return } + + Task { [weak self] in + await self?.database.set(for: action.id, action) + self?.handle(event: Event.track(action, .processing)) + /// Checking if all data is available + guard + let service = self?.bankTransferService.value, + let userId = await service.repository.getUserId(), + let withdrawInfo = try await service.repository.getWithdrawalInfo(userId: userId), + let iban = withdrawInfo.IBAN, + let bic = withdrawInfo.BIC + else { + Logger.log( + event: "Striga Confirm Action", + message: "Absence of data", + logLevel: .error + ) + let regData = try? await self?.bankTransferService.value.getRegistrationData() + self?.handle(event: Event.complete(action, .requestWithdrawInfo(receiver: [regData?.firstName, regData?.lastName].compactMap({ $0 }).joined(separator: " ")))) + return + } + + do { + let result = try await service.repository.initiateSEPAPayment( + userId: userId, + accountId: action.accountId, + amount: action.amount, + iban: iban, + bic: bic + ) + self?.handle(event: Event.complete(action, .initiated(challengeId: result))) + } catch { + self?.handle(event: Event.sendFailure(action, error.localizedDescription)) + } + } + } + + public func handle(event: any UserActionEvent) { + guard let event = event as? Event else { return } + handleInternalEvent(event: event) + } + + func handleInternalEvent(event: Event) { + switch event { + case let .complete(action, result): + Task { [weak self] in + guard let self = self else { return } + let userAction = Action( + id: action.id, + accountId: action.accountId, + amount: action.amount, + status: .ready + ) + userAction.result = result + await self.database.set(for: userAction.id, userAction) + } + case let .track(action, status): + Task { [weak self] in + guard let self = self else { return } + let userAction = Action( + id: action.id, + accountId: action.accountId, + amount: action.amount, + status: status + ) + await self.database.set(for: userAction.id, userAction) + } + case let .sendFailure(action, _): + Task { [weak self] in + guard let userAction = await self?.database.get(for: action.id) else { return } + userAction.status = .error(UserActionError.networkFailure) + await self?.database.set(for: action.id, userAction) + } + } + } +} + +public class OutgoingBankTransferUserAction: UserAction { + public static func == (lhs: OutgoingBankTransferUserAction, rhs: OutgoingBankTransferUserAction) -> Bool { + lhs.id == rhs.id + } + + /// Unique internal id to track. + public let id: String + public let accountId: String + public let amount: String // In cents + /// Abstract status. + public var status: UserActionStatus + public var createdDate: Date + public var updatedDate: Date + public var result: OutgoingBankTransferUserActionResult? + + public init( + id: String, + accountId: String, + amount: String, + status: UserActionStatus, + createdDate: Date = Date(), + updatedDate: Date = Date() + ) { + self.id = id + self.accountId = accountId + self.amount = amount + self.status = status + self.createdDate = createdDate + self.updatedDate = updatedDate + } +} diff --git a/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferUserActionConsumer.swift b/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferUserActionConsumer.swift index 3d85c07565..de0a8f62a4 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferUserActionConsumer.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferUserActionConsumer.swift @@ -175,42 +175,3 @@ public class BankTransferClaimUserAction: UserAction { lhs.id == rhs.id } } - - -public class OutgoingBankTransferUserAction: UserAction { - public static func == (lhs: OutgoingBankTransferUserAction, rhs: OutgoingBankTransferUserAction) -> Bool { - lhs.id == rhs.id - } - - /// Unique internal id to track. - public var id: String - public var accountId: String - public let token: EthereumToken? - public let amount: String? - public let receivingAddress: String - /// Abstract status. - public var status: UserActionStatus - public var createdDate: Date - public var updatedDate: Date - public var result: BankTransferClaimUserActionResult? - - public init( - id: String, - accountId: String, - token: EthereumToken?, - amount: String?, - receivingAddress: String, - status: UserActionStatus, - createdDate: Date = Date(), - updatedDate: Date = Date() - ) { - self.id = id - self.accountId = accountId - self.token = token - self.amount = amount - self.receivingAddress = receivingAddress - self.status = status - self.createdDate = createdDate - self.updatedDate = updatedDate - } -} diff --git a/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift b/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift index 086dff61b8..0df55f91ff 100644 --- a/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift +++ b/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift @@ -549,7 +549,7 @@ final class StrigaRemoteProviderTests: XCTestCase { endDate: Date(), page: 1 ) - debugPrint(result.transactions.first(where: { $0.txType == "SEPA_PAYIN_COMPLETED" })) + // Assert XCTAssertEqual(result.transactions.isEmpty, false) XCTAssertEqual(result.transactions.contains(where: { $0.txType == "SEPA_PAYOUT_COMPLETED" }), false) @@ -559,6 +559,27 @@ final class StrigaRemoteProviderTests: XCTestCase { XCTAssertNotNil(result.transactions.first(where: { $0.txType == "SEPA_PAYIN_COMPLETED" })?.bankingSenderBic) } + func testInitiateSEPAPayment_SuccessfulResponse() async throws { + // Arrange + let mockData = """ + {"challengeId":"924aa8d8-a377-4d61-8761-0b98a4f3f897","dateExpires":"2023-07-25T15:56:11.231Z","transaction":{"syncedOwnerId":"b861b16e-1070-4f54-b992-219549538526","sourceAccountId":"793815a2c66152e7de19318617860ba2","iban":"GB29NWBK60161331926819","bic":"BUKBGB22","amount":"574","status":"PENDING_2FA_CONFIRMATION","txType":"SEPA_PAYOUT_INITIATED","parentWalletId":"f4df3cc6-9c60-461a-8207-05cc8e6e7207","currency":"EUR","feeEstimate":{"totalFee":"0","networkFee":"0","ourFee":"0","theirFee":"0","feeCurrency":"EUR"}},"feeEstimate":{"totalFee":"0","networkFee":"0","ourFee":"0","theirFee":"0","feeCurrency":"EUR"}} + """ + let provider = try getMockProvider(responseString: mockData, statusCode: 200) + + let result = try await provider.initiateSEPAPayment( + userId: "cecaea44-47f2-439b-99a1-a35fefaf1eb6", + accountId: "4dc6ecb29d74198e9e507f8025cad011", + amount: "574", + iban: "GB29NWBK60161331926819", + bic: "BUKBGB22" + ) + + // Assert + XCTAssertNotNil(result.challengeId) + XCTAssertFalse(result.challengeId.isEmpty) + } + + // MARK: - Helper Methods func getMockProvider(responseString: String, statusCode: Int, error: Error? = nil) throws -> StrigaRemoteProvider { diff --git a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c6d7b75310..e186c115a7 100644 --- a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -303,8 +303,8 @@ "repositoryURL": "https://github.com/google/promises.git", "state": { "branch": null, - "revision": "c22f76b709dc4bb6d274398259e75c191e50998a", - "version": "2.3.0" + "revision": "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version": "2.3.1" } }, { diff --git a/p2p_wallet/Injection/Resolver+registerAllServices.swift b/p2p_wallet/Injection/Resolver+registerAllServices.swift index 72eac91f63..7af90e6a00 100644 --- a/p2p_wallet/Injection/Resolver+registerAllServices.swift +++ b/p2p_wallet/Injection/Resolver+registerAllServices.swift @@ -418,6 +418,10 @@ extension Resolver: ResolverRegistering { persistence: resolve(), bankTransferService: resolve(), solanaAccountService: resolve() + ), + StrigaBankTransferOutgoingUserActionConsumer( + persistence: resolve(), + bankTransferService: resolve() ) ] ) diff --git a/p2p_wallet/Resources/Base.lproj/Localizable.strings b/p2p_wallet/Resources/Base.lproj/Localizable.strings index 9b217dfb71..153f551483 100644 --- a/p2p_wallet/Resources/Base.lproj/Localizable.strings +++ b/p2p_wallet/Resources/Base.lproj/Localizable.strings @@ -1475,4 +1475,4 @@ "Receiver" = "Receiver"; "Your bank account name must match the name registered to your Key App account" = "Your bank account name must match the name registered to your Key App account"; "Your IBAN" = "Your IBAN"; -"Outgoing transfer" = "Outgoing transfer"; +"Outcoming transfer" = "Outcoming transfer"; diff --git a/p2p_wallet/Resources/en.lproj/Localizable.strings b/p2p_wallet/Resources/en.lproj/Localizable.strings index dbba900108..fde2f478cb 100644 --- a/p2p_wallet/Resources/en.lproj/Localizable.strings +++ b/p2p_wallet/Resources/en.lproj/Localizable.strings @@ -1474,4 +1474,4 @@ "Receiver" = "Receiver"; "Your bank account name must match the name registered to your Key App account" = "Your bank account name must match the name registered to your Key App account"; "Your IBAN" = "Your IBAN"; -"Outgoing transfer" = "Outgoing transfer"; +"Outcoming transfer" = "Outcoming transfer"; diff --git a/p2p_wallet/Resources/fr.lproj/Localizable.strings b/p2p_wallet/Resources/fr.lproj/Localizable.strings index e5c1f41cd4..a493e4c428 100644 --- a/p2p_wallet/Resources/fr.lproj/Localizable.strings +++ b/p2p_wallet/Resources/fr.lproj/Localizable.strings @@ -1510,4 +1510,4 @@ "Receiver" = "Destinataire"; "Your bank account name must match the name registered to your Key App account" = "Le nom de votre compte bancaire doit correspondre au nom enregistré sur votre compte Key App"; "Your IBAN" = "Votre IBAN"; -"Outgoing transfer" = "Transfert sortant"; +"Outcoming transfer" = "Transfert sortant"; diff --git a/p2p_wallet/Resources/ru.lproj/Localizable.strings b/p2p_wallet/Resources/ru.lproj/Localizable.strings index 794b5beb56..9d1f8ca7cd 100644 --- a/p2p_wallet/Resources/ru.lproj/Localizable.strings +++ b/p2p_wallet/Resources/ru.lproj/Localizable.strings @@ -1506,4 +1506,4 @@ "Receiver" = "Получатель"; "Your bank account name must match the name registered to your Key App account" = "Имя вашего банковского счета должно совпадать с именем, зарегистрированным в вашей учетной записи Key App."; "Your IBAN" = "Ваш IBAN"; -"Outgoing transfer" = "Исходящий перевод"; +"Outcoming transfer" = "Исходящий перевод"; diff --git a/p2p_wallet/Resources/vi.lproj/Localizable.strings b/p2p_wallet/Resources/vi.lproj/Localizable.strings index b349873733..6b0aea7b6f 100644 --- a/p2p_wallet/Resources/vi.lproj/Localizable.strings +++ b/p2p_wallet/Resources/vi.lproj/Localizable.strings @@ -1512,4 +1512,4 @@ "Receiver" = "Người nhận"; "Your bank account name must match the name registered to your Key App account" = "Tên tài khoản ngân hàng của bạn phải khớp với tên đã đăng ký với tài khoản Key App của bạn"; "Your IBAN" = "IBAN của bạn"; -"Outgoing transfer" = "chuyển đi"; +"Outcoming transfer" = "chuyển khoản đi"; diff --git a/p2p_wallet/Scenes/Main/BankTransfer/Claim/StrigaClaimTransaction.swift b/p2p_wallet/Scenes/Main/BankTransfer/Claim/StrigaClaimTransaction.swift index e9b053574b..d735c5b087 100644 --- a/p2p_wallet/Scenes/Main/BankTransfer/Claim/StrigaClaimTransaction.swift +++ b/p2p_wallet/Scenes/Main/BankTransfer/Claim/StrigaClaimTransaction.swift @@ -98,3 +98,5 @@ struct StrigaWithdrawTransaction: StrigaWithdrawTransactionType, StrigaConfirmab return .fakeTransactionSignature(id: UUID().uuidString) } } + +extension StrigaClaimTransaction: Equatable {} diff --git a/p2p_wallet/Scenes/Main/BankTransfer/IBANDetails/IBANDetailsView.swift b/p2p_wallet/Scenes/Main/BankTransfer/IBANDetails/IBANDetailsView.swift index 9a4e3a576c..ee88bf460f 100644 --- a/p2p_wallet/Scenes/Main/BankTransfer/IBANDetails/IBANDetailsView.swift +++ b/p2p_wallet/Scenes/Main/BankTransfer/IBANDetails/IBANDetailsView.swift @@ -58,7 +58,16 @@ struct IBANDetailsView_Previews: PreviewProvider { NavigationView { IBANDetailsView( viewModel: IBANDetailsViewModel( - eurAccount: EURUserAccount(accountID: "", currency: "", createdAt: "", enriched: true, iban: "IBAN", bic: "BIC", bankAccountHolderName: "Name Surname") + eurAccount: EURUserAccount( + accountID: "", + currency: "", + createdAt: "", + enriched: true, + availableBalance: nil, + iban: "IBAN", + bic: "BIC", + bankAccountHolderName: "Name Surname" + ) ) ) .navigationTitle(L10n.euroAccount) diff --git a/p2p_wallet/Scenes/Main/NewHome/HomeCoordinator.swift b/p2p_wallet/Scenes/Main/NewHome/HomeCoordinator.swift index cc74b84002..1dd783f1b8 100644 --- a/p2p_wallet/Scenes/Main/NewHome/HomeCoordinator.swift +++ b/p2p_wallet/Scenes/Main/NewHome/HomeCoordinator.swift @@ -20,13 +20,14 @@ enum HomeNavigation: Equatable { case earn case solanaAccount(SolanaAccount) case claim(EthereumAccount, WormholeClaimUserAction?) - case bankTransferClaim(BankTransferClaimUserAction) + case bankTransferClaim(StrigaClaimTransaction) case actions([WalletActionType]) // HomeEmpty case topUpCoin(Token) case topUp // Top up via bank transfer, bank card or crypto receive case bankTransfer // Only bank transfer - case withdraw + case withdrawCalculator + case withdrawInfo(StrigaWithdrawalInfo) // Error case error(show: Bool) } @@ -183,17 +184,10 @@ final class HomeCoordinator: Coordinator { .map { _ in () } .eraseToAnyPublisher() } - case .bankTransferClaim(let userAction): + case .bankTransferClaim(let transaction): return coordinate(to: BankTransferClaimCoordinator( navigationController: navigationController, - transaction: StrigaClaimTransaction( - challengeId: userAction.result?.challengeId ?? "", - token: userAction.result?.token ?? .usdc, - amount: Double(userAction.amount ?? "") ?? 0, - feeAmount: .zero, - fromAddress: userAction.result?.fromAddress ?? "", - receivingAddress: userAction.receivingAddress - ) + transaction: transaction )) .map { _ in Void() } .eraseToAnyPublisher() @@ -254,9 +248,17 @@ final class HomeCoordinator: Coordinator { case .bankTransfer: return coordinate(to: BankTransferCoordinator(viewController: navigationController)) .eraseToAnyPublisher() - case .withdraw: + case .withdrawCalculator: return coordinate(to: WithdrawCalculatorCoordinator(navigationController: navigationController)) .eraseToAnyPublisher() + case let .withdrawInfo(model): + return coordinate(to: WithdrawCoordinator( + navigationController: navigationController, + strategy: .confirmation, + withdrawalInfo: model + )) + .map { _ in () } // TODO: Handle other actions here + .eraseToAnyPublisher() case let .topUpCoin(token): // SOL, USDC if [Token.nativeSolana, .usdc].contains(token) { diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift index 62426cea97..54a39dc673 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift @@ -75,7 +75,7 @@ struct HomeAccountsView: View { wrappedList(itemsCount: viewModel.accounts.count) { ForEach(viewModel.accounts, id: \.id) { if $0 is OutgoingBankTransferRenderableAccount { - bankTransferCell(rendableAccount: $0, isVisiable: true) + bankTransferCell(rendableAccount: $0, isVisible: true) } else { tokenCell(rendableAccount: $0, isVisiable: true) } @@ -144,12 +144,12 @@ struct HomeAccountsView: View { private func bankTransferCell( rendableAccount: any RenderableAccount, - isVisiable: Bool + isVisible: Bool ) -> some View { HomeBankTransferAccountView( renderable: rendableAccount, - onTap: nil, - onButtonTap: nil + onTap: { viewModel.invoke(for: rendableAccount, event: .tap) }, + onButtonTap: { viewModel.invoke(for: rendableAccount, event: .extraButtonTap) } ) .frame(height: 72) .padding(.horizontal, 16) diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift index 6064e1ccef..9d01d683c2 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift @@ -147,14 +147,12 @@ final class HomeAccountsViewModel: BaseViewModel, ObservableObject { .store(in: &subscriptions) userActionService.$actions - .compactMap { - $0.compactMap { $0 as? BankTransferClaimUserAction } - } - .flatMap { $0.publisher } - .handleEvents(receiveOutput: { val in + .compactMap { $0.compactMap { $0 as? BankTransferClaimUserAction } } + .flatMap(\.publisher) + .handleEvents(receiveOutput: { [weak self] val in switch val.status { - case .error(let error): - self.notificationService.showDefaultErrorNotification() + case .error: + self?.notificationService.showDefaultErrorNotification() default: break } @@ -162,9 +160,28 @@ final class HomeAccountsViewModel: BaseViewModel, ObservableObject { .filter { $0.status == .ready } .receive(on: RunLoop.main) .sink { [weak self] action in - self?.navigation.send(.bankTransferClaim(action)) + guard let result = action.result else { return } + self?.handleClaim(result: result, in: action) }.store(in: &subscriptions) + userActionService.$actions + .compactMap { $0.compactMap { $0 as? OutgoingBankTransferUserAction } } + .flatMap(\.publisher) + .filter { $0.status != .pending && $0.status != .processing } + .receive(on: RunLoop.main) + .sink { [weak self] action in + switch action.status { + case .ready: + guard let result = action.result else { return } + self?.handleOutgoingConfirm(result: result, in: action) + case .error: + self?.notificationService.showDefaultErrorNotification() + default: + break + } + } + .store(in: &subscriptions) + analyticsManager.log(event: .claimAvailable(claim: available(.ethAddressEnabled))) } @@ -204,11 +221,27 @@ final class HomeAccountsViewModel: BaseViewModel, ObservableObject { case let renderableAccount as BankTransferRenderableAccount: handleBankTransfer(account: renderableAccount) + case let renderableAccount as OutgoingBankTransferRenderableAccount: + handleBankTransfer(account: renderableAccount) + default: break } } + private func handleBankTransfer(account: OutgoingBankTransferRenderableAccount) { + let userActionService: UserActionService = Resolver.resolve() + guard account.status != .isProcessing else { return } + let userAction = OutgoingBankTransferUserAction( + id: account.id, + accountId: account.accountId, + amount: String(account.rawAmount), + status: .processing + ) + // Execute and emit action. + userActionService.execute(action: userAction) + } + private func handleBankTransfer(account: BankTransferRenderableAccount) { let userActionService: UserActionService = Resolver.resolve() let userWalletManager: UserWalletManager = Resolver.resolve() @@ -232,6 +265,33 @@ final class HomeAccountsViewModel: BaseViewModel, ObservableObject { userActionService.execute(action: userAction) } + private func handleOutgoingConfirm(result: OutgoingBankTransferUserActionResult, in action: OutgoingBankTransferUserAction) { + switch result { + case let .initiated(challengeId): + self.navigation.send(.bankTransferClaim(StrigaClaimTransaction( + challengeId: challengeId, + token: .usdc, + amount: Double(action.amount) ?? 0 / 100, + feeAmount: .zero, + fromAddress: "", + receivingAddress: "" + ))) + case let .requestWithdrawInfo(receiver): + self.navigation.send(.withdrawInfo(StrigaWithdrawalInfo(receiver: receiver))) + } + } + + private func handleClaim(result: BankTransferClaimUserActionResult, in action: BankTransferClaimUserAction) { + self.navigation.send(.bankTransferClaim(StrigaClaimTransaction( + challengeId: action.result?.challengeId ?? "", + token: action.result?.token ?? .usdc, + amount: Double(action.amount ?? "") ?? 0, + feeAmount: .zero, + fromAddress: action.result?.fromAddress ?? "", + receivingAddress: action.receivingAddress + ))) + } + func actionClicked(_ action: WalletActionType) { switch action { case .receive: @@ -254,7 +314,7 @@ final class HomeAccountsViewModel: BaseViewModel, ObservableObject { case .topUp: navigation.send(.topUp) case .withdraw: - navigation.send(.withdraw) + navigation.send(.withdrawCalculator) } } diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/BankTransferRenderableAccount.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/BankTransferRenderableAccount.swift index 3c741d5359..127e21de33 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/BankTransferRenderableAccount.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/BankTransferRenderableAccount.swift @@ -1,5 +1,6 @@ import BankTransfer import BigInt +import BigDecimal import Foundation import KeyAppBusiness import KeyAppKitCore @@ -91,8 +92,13 @@ private extension Int { struct OutgoingBankTransferRenderableAccount: RenderableAccount { let accountId: String let fiat: Fiat - let visibleAmount: Double + let rawAmount: Int var status: RenderableEthereumAccount.Status + private var amount: CurrencyAmount { + CurrencyAmount(value: BigDecimal(floatLiteral: visibleAmount), currencyCode: fiat.code) + } + + var visibleAmount: Double { Double(rawAmount) / 100 } var id: String { accountId } @@ -100,10 +106,10 @@ struct OutgoingBankTransferRenderableAccount: RenderableAccount { var wrapped: Bool { false } - var title: String { L10n.outgoingTransfer } + var title: String { L10n.outcomingTransfer } var subtitle: String { - String(format: "%.2f", visibleAmount) + " \(fiat.code)" + CurrencyFormatter(defaultValue: "", hideSymbol: true).string(amount: amount).appending(" \(fiat.code)") } var detail: AccountDetail { @@ -173,12 +179,12 @@ class BankTransferRenderableAccountFactory { ) } - if let eur = accounts.eur { + if let eur = accounts.eur, let balance = eur.availableBalance, balance > 0 { transactions.append( - OutgoingBankTransferRenderableAccount( + OutgoingBankTransferRenderableAccount( accountId: eur.accountID, fiat: .eur, - visibleAmount: 1, + rawAmount: balance, status: .ready ) ) diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Subview/HomeBankTransferAccountView.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Subview/HomeBankTransferAccountView.swift index 5cef23e0d8..61915f0ea3 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Subview/HomeBankTransferAccountView.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Subview/HomeBankTransferAccountView.swift @@ -3,7 +3,6 @@ import SwiftUI import KeyAppUI struct HomeBankTransferAccountView: View { - let iconSize: CGFloat = 50 let renderable: any RenderableAccount let onTap: (() -> Void)?