Skip to content

Commit

Permalink
Merge pull request #1497 from p2p-org/feature/pwn-9259
Browse files Browse the repository at this point in the history
[PWN-9259] Home screen outgoing transfer
  • Loading branch information
lisemyon authored Jul 26, 2023
2 parents 42af027 + d7420aa commit b846d12
Show file tree
Hide file tree
Showing 39 changed files with 703 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 @@ -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,
Expand Down
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
4 changes: 4 additions & 0 deletions p2p_wallet/Injection/Resolver+registerAllServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,10 @@ extension Resolver: ResolverRegistering {
persistence: resolve(),
bankTransferService: resolve(),
solanaAccountService: resolve()
),
StrigaBankTransferOutgoingUserActionConsumer(
persistence: resolve(),
bankTransferService: resolve()
)
]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions p2p_wallet/Resources/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
1 change: 1 addition & 0 deletions p2p_wallet/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading

0 comments on commit b846d12

Please sign in to comment.