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 dcaee36e18..de0a8f62a4 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferUserActionConsumer.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/UserAction/StrigaBankTransferUserActionConsumer.swift @@ -104,7 +104,7 @@ public class StrigaBankTransferUserActionConsumer: UserActionConsumer { case let .complete(action, result): Task { [weak self] in guard let self = self else { return } - var userAction = Action( + let userAction = Action( id: action.id, accountId: action.accountId, token: action.token, 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/Assets.xcassets/icon-upload.imageset/Contents.json b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Contents.json index 0370023f0c..bf014c4641 100644 --- a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Contents.json +++ b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Contents.json @@ -1,17 +1,16 @@ { "images" : [ { - "filename" : "Upload.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "Upload@2x.png", + "filename" : "Icon@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "Upload@3x.png", + "filename" : "Icon@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Icon@2x.png b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Icon@2x.png new file mode 100644 index 0000000000..b8a72eeb20 Binary files /dev/null and b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Icon@2x.png differ diff --git a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Icon@3x.png b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Icon@3x.png new file mode 100644 index 0000000000..d4f5066ae1 Binary files /dev/null and b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Icon@3x.png differ diff --git a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload.png b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload.png deleted file mode 100644 index 1f14eabdd4..0000000000 Binary files a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload.png and /dev/null differ diff --git a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload@2x.png b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload@2x.png deleted file mode 100644 index ef20bcdda7..0000000000 Binary files a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload@2x.png and /dev/null differ diff --git a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload@3x.png b/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload@3x.png deleted file mode 100644 index 38b25a0688..0000000000 Binary files a/p2p_wallet/Resources/Assets.xcassets/icon-upload.imageset/Upload@3x.png and /dev/null differ diff --git a/p2p_wallet/Resources/Base.lproj/Localizable.strings b/p2p_wallet/Resources/Base.lproj/Localizable.strings index 35f6863ace..153f551483 100644 --- a/p2p_wallet/Resources/Base.lproj/Localizable.strings +++ b/p2p_wallet/Resources/Base.lproj/Localizable.strings @@ -1475,3 +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"; +"Outcoming transfer" = "Outcoming transfer"; diff --git a/p2p_wallet/Resources/en.lproj/Localizable.strings b/p2p_wallet/Resources/en.lproj/Localizable.strings index e6ca5de3e6..fde2f478cb 100644 --- a/p2p_wallet/Resources/en.lproj/Localizable.strings +++ b/p2p_wallet/Resources/en.lproj/Localizable.strings @@ -1474,3 +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"; +"Outcoming transfer" = "Outcoming transfer"; diff --git a/p2p_wallet/Resources/fr.lproj/Localizable.strings b/p2p_wallet/Resources/fr.lproj/Localizable.strings index 93ea74e17b..a493e4c428 100644 --- a/p2p_wallet/Resources/fr.lproj/Localizable.strings +++ b/p2p_wallet/Resources/fr.lproj/Localizable.strings @@ -1510,3 +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"; +"Outcoming transfer" = "Transfert sortant"; diff --git a/p2p_wallet/Resources/ru.lproj/Localizable.strings b/p2p_wallet/Resources/ru.lproj/Localizable.strings index bfd970f348..9d1f8ca7cd 100644 --- a/p2p_wallet/Resources/ru.lproj/Localizable.strings +++ b/p2p_wallet/Resources/ru.lproj/Localizable.strings @@ -1506,3 +1506,4 @@ "Receiver" = "Получатель"; "Your bank account name must match the name registered to your Key App account" = "Имя вашего банковского счета должно совпадать с именем, зарегистрированным в вашей учетной записи Key App."; "Your IBAN" = "Ваш IBAN"; +"Outcoming transfer" = "Исходящий перевод"; diff --git a/p2p_wallet/Resources/vi.lproj/Localizable.strings b/p2p_wallet/Resources/vi.lproj/Localizable.strings index 08c9ffb428..6b0aea7b6f 100644 --- a/p2p_wallet/Resources/vi.lproj/Localizable.strings +++ b/p2p_wallet/Resources/vi.lproj/Localizable.strings @@ -1512,3 +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"; +"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/BankTransfer/Withdraw/WithdrawCoordinator.swift b/p2p_wallet/Scenes/Main/BankTransfer/Withdraw/WithdrawCoordinator.swift index 0e1179e033..fa4f48913e 100644 --- a/p2p_wallet/Scenes/Main/BankTransfer/Withdraw/WithdrawCoordinator.swift +++ b/p2p_wallet/Scenes/Main/BankTransfer/Withdraw/WithdrawCoordinator.swift @@ -34,8 +34,8 @@ final class WithdrawCoordinator: Coordinator { .map { WithdrawCoordinator.Result.canceled }, viewModel.actionCompletedPublisher .map { WithdrawCoordinator.Result.verified } - .handleEvents(receiveOutput: { _ in - self.navigationController.popToRootViewController(animated: true) + .handleEvents(receiveOutput: { [weak self] _ in + self?.navigationController.popToRootViewController(animated: true) }) ) .prefix(1).eraseToAnyPublisher() diff --git a/p2p_wallet/Scenes/Main/BankTransfer/Withdraw/WithdrawViewModel.swift b/p2p_wallet/Scenes/Main/BankTransfer/Withdraw/WithdrawViewModel.swift index ec584fee26..4ed155b8f1 100644 --- a/p2p_wallet/Scenes/Main/BankTransfer/Withdraw/WithdrawViewModel.swift +++ b/p2p_wallet/Scenes/Main/BankTransfer/Withdraw/WithdrawViewModel.swift @@ -16,7 +16,7 @@ class WithdrawViewModel: BaseViewModel, ObservableObject { @Published var IBAN: String = "" @Published var BIC: String = "" @Published var receiver: String = "" - @Published var actionTitle: String = "Withdraw" + @Published var actionTitle: String = L10n.withdraw @Published var isDataValid = false @Published var fieldsStatuses = [WithdrawViewField: FieldStatus]() @Published var isLoading = false @@ -50,7 +50,7 @@ class WithdrawViewModel: BaseViewModel, ObservableObject { isDataValid = fields.values.filter({ status in status == .valid }).count == fields.keys.count - actionTitle = isDataValid ? L10n.withdrawal : L10n.checkYourData + actionTitle = isDataValid ? L10n.withdraw : L10n.checkYourData }) .assignWeak(to: \.fieldsStatuses, on: self) .store(in: &subscriptions) 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/Aggregator/HomeAccountsAggregator.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Aggregator/HomeAccountsAggregator.swift index 4c4cec60a1..ef74ccba79 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Aggregator/HomeAccountsAggregator.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Aggregator/HomeAccountsAggregator.swift @@ -8,7 +8,7 @@ struct HomeAccountsAggregator: DataAggregator { input: ( solanaAccounts: [RenderableSolanaAccount], ethereumAccounts: [RenderableEthereumAccount], - bankTransferAccounts: [BankTransferRenderableAccount] + bankTransferAccounts: [any RenderableAccount] ) ) -> (primary: [any RenderableAccount], secondary: [any RenderableAccount]) { diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Aggregator/HomeEthereumAccountsAggregator.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Aggregator/HomeEthereumAccountsAggregator.swift index caa2339166..98d4beea09 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Aggregator/HomeEthereumAccountsAggregator.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Aggregator/HomeEthereumAccountsAggregator.swift @@ -31,7 +31,7 @@ struct HomeEthereumAccountsAggregator: DataAggregator { // Compare using fiat. if balanceInFiat >= CurrencyAmount(usd: 5) { // Balance is greater than $1, user can claim. - status = .readyToClaim + status = .ready } else { // Balance is to low. status = .balanceToLow @@ -40,7 +40,7 @@ struct HomeEthereumAccountsAggregator: DataAggregator { // Compare using crypto amount. if account.balance > 0 { // Balance is not zero - status = .readyToClaim + status = .ready } else { // Balance is to low. status = .balanceToLow @@ -49,7 +49,7 @@ struct HomeEthereumAccountsAggregator: DataAggregator { } else { // Claiming is running. - status = .isClaimming + status = .isProcessing } return RenderableEthereumAccount( diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift index dbc90ad515..54a39dc673 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsView.swift @@ -74,7 +74,11 @@ struct HomeAccountsView: View { .padding(.top, 32) wrappedList(itemsCount: viewModel.accounts.count) { ForEach(viewModel.accounts, id: \.id) { - tokenCell(rendableAccount: $0, isVisiable: true) + if $0 is OutgoingBankTransferRenderableAccount { + bankTransferCell(rendableAccount: $0, isVisible: true) + } else { + tokenCell(rendableAccount: $0, isVisiable: true) + } } } if !viewModel.hiddenAccounts.isEmpty { @@ -138,6 +142,19 @@ struct HomeAccountsView: View { .padding(.horizontal, 16) } + private func bankTransferCell( + rendableAccount: any RenderableAccount, + isVisible: Bool + ) -> some View { + HomeBankTransferAccountView( + renderable: rendableAccount, + onTap: { viewModel.invoke(for: rendableAccount, event: .tap) }, + onButtonTap: { viewModel.invoke(for: rendableAccount, event: .extraButtonTap) } + ) + .frame(height: 72) + .padding(.horizontal, 16) + } + @ViewBuilder private func wrappedList( itemsCount: Int, diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift index 3cd0bafbc1..9d01d683c2 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/HomeAccountsViewModel.swift @@ -107,39 +107,15 @@ final class HomeAccountsViewModel: BaseViewModel, ObservableObject { // bankTransferPublisher let bankTransferServicePublisher = Publishers.CombineLatest( bankTransferService.value.state - .compactMap { $0.value.wallet?.accounts.usdc }, - userActionService.$actions.map { userActions in - userActions.compactMap { $0 as? BankTransferClaimUserAction } - } + .compactMap { $0.value.wallet?.accounts }, + userActionService.$actions + ) - .compactMap { (account, actions) -> [BankTransferRenderableAccount]? in - guard - account.availableBalance > 0, - let address = try? EthereumAddress( - hex: EthereumAddresses.ERC20.usdc.rawValue, - eip55: false - ) else { return nil } - - let token = EthereumToken( - name: SolanaToken.usdc.name, - symbol: SolanaToken.usdc.symbol, - decimals: 6, - logo: URL(string: SolanaToken.usdc.logoURI ?? ""), - contractType: .erc20(contract: address) + .compactMap { (account, actions) -> [any RenderableAccount] in + BankTransferRenderableAccountFactory.renderableAccount( + accounts: account, + actions: actions ) - - let action = actions.first(where: { action in - action.id == account.accountID - }) - return [ - BankTransferRenderableAccount( - accountId: account.accountID, - token: token, - visibleAmount: account.availableBalance, - rawAmount: account.totalBalance, - status: action?.status == .processing ? .isClaimming : .readyToClaim - ) - ] } let homeAccountsAggregator = HomeAccountsAggregator() @@ -150,7 +126,8 @@ final class HomeAccountsViewModel: BaseViewModel, ObservableObject { bankTransferServicePublisher.prepend([]) ) .map { solanaAccounts, ethereumAccounts, bankTransferAccounts in - homeAccountsAggregator.transform(input: (solanaAccounts, ethereumAccounts, bankTransferAccounts)) + homeAccountsAggregator + .transform(input: (solanaAccounts, ethereumAccounts, bankTransferAccounts)) } .receive(on: RunLoop.main) .sink { primary, secondary in @@ -170,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 } @@ -185,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))) } @@ -227,16 +221,32 @@ 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() guard - account.status != .isClaimming, + account.status != .isProcessing, let walletPubKey = userWalletManager.wallet?.account.publicKey else { return } let userAction = BankTransferClaimUserAction( @@ -255,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: @@ -277,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 ff0b1a8b6f..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,7 +1,11 @@ import BankTransfer +import BigInt +import BigDecimal import Foundation +import KeyAppBusiness import KeyAppKitCore -import BigInt +import Web3 +import Wormhole struct BankTransferRenderableAccount: RenderableAccount { let accountId: String @@ -43,9 +47,9 @@ struct BankTransferRenderableAccount: RenderableAccount { var detail: AccountDetail { switch status { - case .readyToClaim: + case .ready: return .button(label: L10n.claim, enabled: true) - case .isClaimming: + case .isProcessing: return .button(label: L10n.claim, enabled: true) case .balanceToLow: return .text("") @@ -71,7 +75,7 @@ struct BankTransferRenderableAccount: RenderableAccount { var isLoading: Bool { switch status { - case .isClaimming: + case .isProcessing: return true default: return false @@ -84,3 +88,107 @@ private extension Int { Double(self * 10_000) } } + +struct OutgoingBankTransferRenderableAccount: RenderableAccount { + let accountId: String + let fiat: Fiat + 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 } + + var icon: AccountIcon { .image(.iconUpload) } + + var wrapped: Bool { false } + + var title: String { L10n.outcomingTransfer } + + var subtitle: String { + CurrencyFormatter(defaultValue: "", hideSymbol: true).string(amount: amount).appending(" \(fiat.code)") + } + + var detail: AccountDetail { + switch status { + case .ready, .isProcessing: + return .button(label: L10n.confirm, enabled: true) + case .balanceToLow: + return .text("") + } + } + + var extraAction: AccountExtraAction? { nil } + + var tags: AccountTags { + var tags: AccountTags = [] + + if status == .balanceToLow { + if visibleAmount == 0 { + tags.insert(.hidden) + } else { + tags.insert(.ignore) + } + } + return tags + } + + var isLoading: Bool { + switch status { + case .isProcessing: + return true + default: + return false + } + } +} + +class BankTransferRenderableAccountFactory { + static func renderableAccount(accounts: UserAccounts, actions: [any UserAction]) -> [any RenderableAccount] { + var transactions = [any RenderableAccount]() + if + let usdc = accounts.usdc, + usdc.availableBalance > 0, + let address = try? EthereumAddress( + hex: EthereumAddresses.ERC20.usdc.rawValue, + eip55: false + ) { + let token = EthereumToken( + name: SolanaToken.usdc.name, + symbol: SolanaToken.usdc.symbol, + decimals: 6, + logo: URL(string: SolanaToken.usdc.logoURI ?? ""), + contractType: .erc20(contract: address) + ) + let action = actions + .compactMap { $0 as? BankTransferClaimUserAction } + .first(where: { action in + action.id == usdc.accountID + }) + transactions.append( + BankTransferRenderableAccount( + accountId: usdc.accountID, + token: token, + visibleAmount: usdc.availableBalance, + rawAmount: usdc.totalBalance, + status: action?.status == .processing ? .isProcessing : .ready + ) + ) + } + + if let eur = accounts.eur, let balance = eur.availableBalance, balance > 0 { + transactions.append( + OutgoingBankTransferRenderableAccount( + accountId: eur.accountID, + fiat: .eur, + rawAmount: balance, + status: .ready + ) + ) + } + return transactions + } +} diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/RendableAccount+EthereumAccount.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/RendableAccount+EthereumAccount.swift index ce12aae3c2..6520cac258 100644 --- a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/RendableAccount+EthereumAccount.swift +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Model/RendableAccount+EthereumAccount.swift @@ -47,9 +47,9 @@ struct RenderableEthereumAccount: RenderableAccount { var detail: AccountDetail { switch status { - case .readyToClaim: + case .ready: return .button(label: L10n.claim, enabled: true) - case .isClaimming: + case .isProcessing: return .button(label: L10n.claiming, enabled: false) case .balanceToLow: if let balanceInFiat = account.balanceInFiat { @@ -83,8 +83,8 @@ struct RenderableEthereumAccount: RenderableAccount { extension RenderableEthereumAccount { enum Status: Equatable { - case readyToClaim - case isClaimming + case ready + case isProcessing case balanceToLow } } diff --git a/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Subview/HomeBankTransferAccountView.swift b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Subview/HomeBankTransferAccountView.swift new file mode 100644 index 0000000000..61915f0ea3 --- /dev/null +++ b/p2p_wallet/Scenes/Main/NewHome/Subview/AccountList/Subview/HomeBankTransferAccountView.swift @@ -0,0 +1,31 @@ +import Foundation +import SwiftUI +import KeyAppUI + +struct HomeBankTransferAccountView: View { + let renderable: any RenderableAccount + + let onTap: (() -> Void)? + let onButtonTap: (() -> Void)? + + var body: some View { + FinanceBlockView( + leadingItem: FinanceBlockLeadingItem( + image: .image(.iconUpload), + iconSize: CGSize(width: 50, height: 50), + isWrapped: false + ), + centerItem: FinancialBlockCenterItem( + title: renderable.title, + subtitle: renderable.subtitle + ), + trailingItem: FinancialBlockTrailingItem( + isLoading: renderable.isLoading, + detail: renderable.detail + ) + ) + .onTapGesture { + onTap?() + } + } +} diff --git a/p2p_wallet/Scenes/Main/Receive/Model/SpacerReceiveItem.swift b/p2p_wallet/Scenes/Main/Receive/Model/SpacerReceiveItem.swift index 9d87621b95..c3343b0ef6 100644 --- a/p2p_wallet/Scenes/Main/Receive/Model/SpacerReceiveItem.swift +++ b/p2p_wallet/Scenes/Main/Receive/Model/SpacerReceiveItem.swift @@ -4,11 +4,12 @@ import SwiftUI struct SpacerReceiveItem { var id: String = UUID().uuidString var height: CGFloat = 8 + var width: CGFloat = 8 } extension SpacerReceiveItem: ReceiveRendableItem { func render() -> some View { Color(UIColor.clear) - .frame(height: height) + .frame(width: width, height: height) } } diff --git a/p2p_wallet/UI/SwiftUI/Renderable/FinanceBlockView.swift b/p2p_wallet/UI/SwiftUI/Renderable/FinanceBlockView.swift new file mode 100644 index 0000000000..1c462ef92c --- /dev/null +++ b/p2p_wallet/UI/SwiftUI/Renderable/FinanceBlockView.swift @@ -0,0 +1,161 @@ +import SwiftUI +import KeyAppUI + +struct FinanceBlockView: View { + @State var leadingItem: any Renderable + @State var centerItem: any Renderable + @State var trailingItem: any Renderable + + var leadingView: some View { + AnyView(leadingItem.render()) + } + + var centerView: some View { + AnyView(centerItem.render()) + } + + var trailingView: some View { + AnyView(trailingItem.render()) + } + + var body: some View { + HStack(spacing: 0) { + HStack(spacing: 12) { + leadingView + centerView + } + Spacer() + trailingView + } + .padding(.vertical, 12) + } +} + + +struct FinanceBlockView_Previews: PreviewProvider { + static var previews: some View { + FinanceBlockView( + leadingItem: FinanceBlockLeadingItem(image: .image(.iconUpload), iconSize: CGSize(width: 50, height: 50), isWrapped: false), + centerItem: FinancialBlockCenterItem( + title: "renderable.title", + subtitle: "renderable.subtitle" + ), + trailingItem: ListSpacerCellViewItem(height: 0, backgroundColor: .clear) + ) + } +} + +// MARK: - Leading + +struct FinanceBlockLeadingItem: Renderable { + typealias ViewType = FinanceBlockLeadingView + var id: String = UUID().uuidString + + // TODO: Get rid of AccountIcon + var image: AccountIcon + var iconSize: CGSize + var isWrapped: Bool + + func render() -> FinanceBlockLeadingView { + FinanceBlockLeadingView(item: self) + } +} + +struct FinanceBlockLeadingView: View { + let item: FinanceBlockLeadingItem + + var body: some View { + var anURL: URL? + var aSeed: String? + var anImage: UIImage? + switch item.image { + case let .url(url): + anURL = url + case let .image(image): + anImage = image + case let .random(seed): + aSeed = seed + } + return CoinLogoImageViewRepresentable( + size: item.iconSize.width, + args: .manual( + preferredImage: anImage, + url: anURL, + key: aSeed ?? "", + wrapped: item.isWrapped + ) + ) + .frame(width: item.iconSize.width, height: item.iconSize.height) + } +} + +// MARK: - Center + +struct FinancialBlockCenterItem: Renderable { + typealias ViewType = FinancialBlockCenterView + var id: String = UUID().uuidString + + var title: String? + var subtitle: String? + + func render() -> FinancialBlockCenterView { + FinancialBlockCenterView(item: self) + } +} + +struct FinancialBlockCenterView: View { + let item: FinancialBlockCenterItem + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + if let title = item.title { + Text(title) + .apply(style: .text2) + .foregroundColor(Color(Asset.Colors.night.color)) + } + if let subtitle = item.subtitle { + Text(subtitle) + .apply(style: .label1) + .foregroundColor(Color(Asset.Colors.mountain.color)) + } + } + } +} + +// MARK: - Trailing + +struct FinancialBlockTrailingItem: Renderable { + typealias ViewType = FinancialBlockTrailingView + + var id: String = UUID().uuidString + var isLoading: Bool + var detail: AccountDetail + var onButtonTap: (() -> Void)? + + func render() -> FinancialBlockTrailingView { + FinancialBlockTrailingView(item: self) + } +} + +struct FinancialBlockTrailingView: View { + let item: FinancialBlockTrailingItem + + var body: some View { + switch item.detail { + case let .text(text): + Text(text) + .fontWeight(.semibold) + .apply(style: .text3) + .foregroundColor(Color(Asset.Colors.night.color)) + case let .button(text, enabled): + NewTextButton( + title: text, + size: .small, + style: .primaryWhite, + isEnabled: enabled, + isLoading: item.isLoading, + action: { item.onButtonTap?() } + ) + } + } +}