Skip to content

Commit

Permalink
Merge pull request #1505 from p2p-org/feature/PWN-9259_confirmButton
Browse files Browse the repository at this point in the history
[PWN-9259] Finish confirm button action handlers
  • Loading branch information
sidorov-panda authored Jul 26, 2023
2 parents e0033f3 + f25f953 commit d7420aa
Show file tree
Hide file tree
Showing 26 changed files with 370 additions and 84 deletions.
13 changes: 12 additions & 1 deletion Packages/KeyAppKit/Sources/BankTransfer/Models/UserWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

public struct StrigaInitiateSEPAPaymentResponse: Codable {
let challengeId: String
let dateExpires: String
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<T: Decodable>(userId: String, accountId: String) async throws -> T {
try await remoteProvider.enrichAccount(userId: userId, accountId: accountId)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Action> = .init()

private var bankTransferService: AnyBankTransferService<StrigaBankTransferUserDataRepository>

public init(
persistence: UserActionPersistentStorage,
bankTransferService: AnyBankTransferService<StrigaBankTransferUserDataRepository>
) {
self.persistence = persistence
self.bankTransferService = bankTransferService
}

public var onUpdate: AnyPublisher<any UserAction, Never> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
{
Expand Down
Loading

0 comments on commit d7420aa

Please sign in to comment.