Skip to content

Commit 8ac93f4

Browse files
Add Pay with Transfer (bank transfer) payment method
Introduce a new bank transfer charge flow spanning both PaystackCore and PaystackUI. PaystackCore: - Add payWithTransfer endpoint (PayWithTransferService + Paystack extension) with request/response models - Add PayWithTransferPusherResponse for subscription-based status updates - Add MerchantChannelSettings and wire bank_transfer into channel options PaystackUI: - Add BankTransfer flow (Views, ViewModel, Repository, Models) with a state machine covering account details, confirming, delayed confirmation, taking-longer, and refund-initiated phases - Add shared components: AccountDetailRow, ChangePaymentMethodFooter, TimelineNode, and a bank picker sheet - Surface bank transfer as a selectable channel in the charge flow Tests: - Cover the endpoint, status mapping, view model, repository, and provider catalog, plus Pusher fixtures for each transfer status
1 parent 16bcfd6 commit 8ac93f4

54 files changed

Lines changed: 3201 additions & 63 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Package.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ let package = Package(
4343
.copy("API/Transactions/Resources/VerifyAccessCode.json"),
4444
.copy("API/Charge/Resources/ChargeAuthenticationResponse.json"),
4545
.copy("API/Other/Resources/AddressStatesResponse.json"),
46-
.copy("API/Charge/Resources/ChargeMobileMoneyResponse.json")
46+
.copy("API/Charge/Resources/ChargeMobileMoneyResponse.json"),
47+
.copy("API/Charge/Resources/PayWithTransferResponse.json"),
48+
.copy("API/Charge/Resources/PayWithTransferPusherSuccess.json"),
49+
.copy("API/Charge/Resources/PayWithTransferPusherCreditReceived.json"),
50+
.copy("API/Charge/Resources/PayWithTransferPusherCreditPending.json"),
51+
.copy("API/Charge/Resources/PayWithTransferPusherCreditRejected.json"),
52+
.copy("API/Charge/Resources/PayWithTransferPusherIncorrectAmount.json")
4753

4854
])
4955
]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Foundation
2+
3+
/// Public Pay-with-Transfer surface. Used by the UI module to provision a
4+
/// virtual bank account and listen for Pusher status updates ; can also be
5+
/// called directly by integrators driving their own UI on top of
6+
/// `PaystackCore`.
7+
public extension Paystack {
8+
9+
private var service: PayWithTransferService {
10+
return PayWithTransferServiceImplementation(config: config)
11+
}
12+
13+
/// Provisions a one-time virtual bank account for the customer to make
14+
/// a transfer to. The response's `pusherChannel` should be subscribed
15+
/// to via ``listenForTransferResponse(onChannel:)`` for real-time
16+
/// status updates ; the response's `accountExpiresAt` drives the
17+
/// customer-facing countdown.
18+
///
19+
/// - Parameter request: Required configuration for the virtual account.
20+
/// `preferredProvider` is optional — when omitted, the backend picks
21+
/// a default (typically `paystack-titan`).
22+
/// - Returns: A ``Service`` with the ``PayWithTransferResponse`` payload.
23+
func payWithTransfer(_ request: PayWithTransferRequest)
24+
-> Service<PayWithTransferResponse> {
25+
return service.postPayWithTransfer(request)
26+
}
27+
28+
/// Listens for Pay-with-Transfer status updates on the Pusher channel
29+
/// returned by ``payWithTransfer(_:)``. The underlying listener is
30+
/// single-shot per the existing `PusherSubscriptionListener` contract,
31+
/// so callers that need to keep listening through transient statuses
32+
/// must re-subscribe after each event.
33+
///
34+
/// - Parameter channelName: The `pusherChannel` value returned from
35+
/// `payWithTransfer` (e.g. `PWT6215047322`).
36+
/// - Returns: A ``Service`` carrying a ``PayWithTransferPusherResponse``
37+
/// on the first event the channel emits.
38+
func listenForTransferResponse(onChannel channelName: String)
39+
-> Service<PayWithTransferPusherResponse> {
40+
let subscription: any Subscription = PusherSubscription(
41+
channelName: channelName, eventName: "response")
42+
return Service(subscription)
43+
}
44+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
protocol PayWithTransferService: PaystackService {
4+
func postPayWithTransfer(_ request: PayWithTransferRequest)
5+
-> Service<PayWithTransferResponse>
6+
}
7+
8+
struct PayWithTransferServiceImplementation: PayWithTransferService {
9+
10+
var config: PaystackConfig
11+
12+
var parentPath: String {
13+
return "checkout"
14+
}
15+
16+
func postPayWithTransfer(_ request: PayWithTransferRequest)
17+
-> Service<PayWithTransferResponse> {
18+
return post("/pay_with_transfer", request)
19+
.asService()
20+
}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
3+
public struct PayWithTransferPusherResponse: Codable, Equatable {
4+
public let status: String
5+
public let message: String
6+
public let data: PayWithTransferEventData?
7+
public let errors: [String]?
8+
}
9+
10+
public struct PayWithTransferEventData: Codable, Equatable {
11+
public let messageType: String?
12+
public let transactionId: String?
13+
public let referenceId: String?
14+
public let reference: String?
15+
public let trxref: String?
16+
public let trans: String?
17+
public let response: String?
18+
public let redirecturl: String?
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
public struct PayWithTransferRequest: Codable, Equatable {
4+
public let fulfilLateNotification: Bool
5+
public let transactionId: Int
6+
public let preferredProvider: String?
7+
8+
public init(fulfilLateNotification: Bool,
9+
transactionId: Int,
10+
preferredProvider: String? = nil) {
11+
self.fulfilLateNotification = fulfilLateNotification
12+
self.transactionId = transactionId
13+
self.preferredProvider = preferredProvider
14+
}
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
3+
public struct PayWithTransferResponse: Codable, Equatable {
4+
public let status: Bool
5+
public let message: String
6+
public let data: PayWithTransferData
7+
}
8+
9+
public struct PayWithTransferData: Codable, Equatable {
10+
public let accountName: String
11+
public let accountNumber: String
12+
public let transactionReference: String
13+
public let bank: TransferBank
14+
public let accountExpiresAt: Date
15+
public let assignmentExpiresAt: Date
16+
public let transactionId: String
17+
public let pusherChannel: String
18+
}
19+
20+
public struct TransferBank: Codable, Equatable {
21+
public let slug: String
22+
public let name: String
23+
public let id: Int
24+
}

Sources/PaystackSDK/Core/Models/Models/VerifyAccessCode/ChannelOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public struct ChannelOptions: Codable {
1717
enum CodingKeys: String, CodingKey {
1818
case ussd
1919
case qrCode = "qr"
20-
case bankTransfer = "bank_transfer"
20+
case bankTransfer
2121
case mobileMoney
2222
}
2323
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
3+
public struct MerchantChannelSettings: Codable, Equatable {
4+
public var bankTransfer: BankTransferMerchantSettings?
5+
6+
public init(bankTransfer: BankTransferMerchantSettings? = nil) {
7+
self.bankTransfer = bankTransfer
8+
}
9+
}
10+
11+
public struct BankTransferMerchantSettings: Codable, Equatable {
12+
public var fulfilLateNotification: Bool?
13+
14+
public init(fulfilLateNotification: Bool? = nil) {
15+
self.fulfilLateNotification = fulfilLateNotification
16+
}
17+
}

Sources/PaystackSDK/Core/Models/Models/VerifyAccessCode/VerifyAccessCodeData.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ public struct VerifyAccessCodeData: Decodable {
1212
public var currency: String
1313
public var channels: [Channel]
1414
public var channelOptions: ChannelOptions
15+
public var merchantChannelSettings: MerchantChannelSettings?
1516
public var publicEncryptionKey: String
1617

1718
public init(id: Int?, email: String, amount: Decimal, reference: String, accessCode: String,
1819
merchantLogo: String? = nil, merchantName: String, domain: Domain,
1920
currency: String, channels: [Channel], channelOptions: ChannelOptions,
21+
merchantChannelSettings: MerchantChannelSettings? = nil,
2022
publicEncryptionKey: String) {
2123
self.id = id
2224
self.email = email
@@ -29,6 +31,7 @@ public struct VerifyAccessCodeData: Decodable {
2931
self.currency = currency
3032
self.channels = channels
3133
self.channelOptions = channelOptions
34+
self.merchantChannelSettings = merchantChannelSettings
3235
self.publicEncryptionKey = publicEncryptionKey
3336
}
3437
}

Sources/PaystackSDK/Core/Utils/DateFormatter.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ public extension DateFormatter {
44

55
static var paystackFormatter: DateFormatter {
66
let formatter = DateFormatter()
7-
formatter.locale = .current
7+
formatter.locale = Locale(identifier: "en_US_POSIX")
8+
formatter.timeZone = TimeZone(identifier: "UTC")
89
formatter.dateFormat = DateFormat.paystack.rawValue
910
return formatter
1011
}

0 commit comments

Comments
 (0)