Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ let package = Package(
.copy("API/Transactions/Resources/VerifyAccessCode.json"),
.copy("API/Charge/Resources/ChargeAuthenticationResponse.json"),
.copy("API/Other/Resources/AddressStatesResponse.json"),
.copy("API/Charge/Resources/ChargeMobileMoneyResponse.json")
.copy("API/Charge/Resources/ChargeMobileMoneyResponse.json"),
.copy("API/Charge/Resources/PayWithTransferResponse.json"),
.copy("API/Charge/Resources/PayWithTransferPusherSuccess.json"),
.copy("API/Charge/Resources/PayWithTransferPusherCreditReceived.json"),
.copy("API/Charge/Resources/PayWithTransferPusherCreditPending.json"),
.copy("API/Charge/Resources/PayWithTransferPusherCreditRejected.json"),
.copy("API/Charge/Resources/PayWithTransferPusherIncorrectAmount.json")

])
]
Expand Down
44 changes: 44 additions & 0 deletions Sources/PaystackSDK/API/Charge/PayWithTransfer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation

/// Public Pay-with-Transfer surface. Used by the UI module to provision a
/// virtual bank account and listen for Pusher status updates ; can also be
/// called directly by integrators driving their own UI on top of
/// `PaystackCore`.
public extension Paystack {

private var service: PayWithTransferService {
return PayWithTransferServiceImplementation(config: config)
}

/// Provisions a one-time virtual bank account for the customer to make
/// a transfer to. The response's `pusherChannel` should be subscribed
/// to via ``listenForTransferResponse(onChannel:)`` for real-time
/// status updates ; the response's `accountExpiresAt` drives the
/// customer-facing countdown.
///
/// - Parameter request: Required configuration for the virtual account.
/// `preferredProvider` is optional — when omitted, the backend picks
/// a default (typically `paystack-titan`).
/// - Returns: A ``Service`` with the ``PayWithTransferResponse`` payload.
func payWithTransfer(_ request: PayWithTransferRequest)
-> Service<PayWithTransferResponse> {
return service.postPayWithTransfer(request)
}

/// Listens for Pay-with-Transfer status updates on the Pusher channel
/// returned by ``payWithTransfer(_:)``. The underlying listener is
/// single-shot per the existing `PusherSubscriptionListener` contract,
/// so callers that need to keep listening through transient statuses
/// must re-subscribe after each event.
///
/// - Parameter channelName: The `pusherChannel` value returned from
/// `payWithTransfer` (e.g. `PWT6215047322`).
/// - Returns: A ``Service`` carrying a ``PayWithTransferPusherResponse``
/// on the first event the channel emits.
func listenForTransferResponse(onChannel channelName: String)
-> Service<PayWithTransferPusherResponse> {
let subscription: any Subscription = PusherSubscription(
channelName: channelName, eventName: "response")
return Service(subscription)
}
}
21 changes: 21 additions & 0 deletions Sources/PaystackSDK/API/Charge/PayWithTransferService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

protocol PayWithTransferService: PaystackService {
func postPayWithTransfer(_ request: PayWithTransferRequest)
-> Service<PayWithTransferResponse>
}

struct PayWithTransferServiceImplementation: PayWithTransferService {

var config: PaystackConfig

var parentPath: String {
return "checkout"
}

func postPayWithTransfer(_ request: PayWithTransferRequest)
-> Service<PayWithTransferResponse> {
return post("/pay_with_transfer", request)

Check warning on line 18 in Sources/PaystackSDK/API/Charge/PayWithTransferService.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor your code to get this URI from a customizable parameter.

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AZ7LlPMxBW70q7L2CYZW&open=AZ7LlPMxBW70q7L2CYZW&pullRequest=125
.asService()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

public struct PayWithTransferPusherResponse: Codable, Equatable {
public let status: String
public let message: String
public let data: PayWithTransferEventData?
public let errors: [String]?
}

public struct PayWithTransferEventData: Codable, Equatable {
public let messageType: String?
public let transactionId: String?
public let referenceId: String?
public let reference: String?
public let trxref: String?
public let trans: String?
public let response: String?
public let redirecturl: String?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

public struct PayWithTransferRequest: Codable, Equatable {
public let fulfilLateNotification: Bool
public let transactionId: Int
public let preferredProvider: String?

public init(fulfilLateNotification: Bool,
transactionId: Int,
preferredProvider: String? = nil) {
self.fulfilLateNotification = fulfilLateNotification
self.transactionId = transactionId
self.preferredProvider = preferredProvider
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

public struct PayWithTransferResponse: Codable, Equatable {
public let status: Bool
public let message: String
public let data: PayWithTransferData
}

public struct PayWithTransferData: Codable, Equatable {
public let accountName: String
public let accountNumber: String
public let transactionReference: String
public let bank: TransferBank
public let accountExpiresAt: Date
public let assignmentExpiresAt: Date
public let transactionId: String
public let pusherChannel: String
}

public struct TransferBank: Codable, Equatable {
public let slug: String
public let name: String
public let id: Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public struct ChannelOptions: Codable {
enum CodingKeys: String, CodingKey {
case ussd
case qrCode = "qr"
case bankTransfer = "bank_transfer"
case bankTransfer
case mobileMoney
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public struct MerchantChannelSettings: Codable, Equatable {
public var bankTransfer: BankTransferMerchantSettings?

public init(bankTransfer: BankTransferMerchantSettings? = nil) {
self.bankTransfer = bankTransfer
}
}

public struct BankTransferMerchantSettings: Codable, Equatable {
public var fulfilLateNotification: Bool?

public init(fulfilLateNotification: Bool? = nil) {
self.fulfilLateNotification = fulfilLateNotification
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ public struct VerifyAccessCodeData: Decodable {
public var currency: String
public var channels: [Channel]
public var channelOptions: ChannelOptions
public var merchantChannelSettings: MerchantChannelSettings?
public var publicEncryptionKey: String

public init(id: Int?, email: String, amount: Decimal, reference: String, accessCode: String,
merchantLogo: String? = nil, merchantName: String, domain: Domain,
currency: String, channels: [Channel], channelOptions: ChannelOptions,
merchantChannelSettings: MerchantChannelSettings? = nil,
publicEncryptionKey: String) {
self.id = id
self.email = email
Expand All @@ -29,6 +31,7 @@ public struct VerifyAccessCodeData: Decodable {
self.currency = currency
self.channels = channels
self.channelOptions = channelOptions
self.merchantChannelSettings = merchantChannelSettings
self.publicEncryptionKey = publicEncryptionKey
}
}
3 changes: 2 additions & 1 deletion Sources/PaystackSDK/Core/Utils/DateFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ public extension DateFormatter {

static var paystackFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.locale = .current
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.dateFormat = DateFormat.paystack.rawValue
return formatter
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

struct BankTransferConfig: Equatable {
let fulfilLateNotification: Bool

let transactionId: Int

let availableProviders: [String]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
import PaystackCore

struct BankTransferDetails: Equatable {
let accountName: String
let accountNumber: String
let bankName: String
let bankSlug: String
let transactionReference: String
let pusherChannel: String
let accountExpiresAt: Date
let transactionId: String
}

extension BankTransferDetails {

static func from(_ response: PayWithTransferResponse) -> Self {
BankTransferDetails(
accountName: response.data.accountName,
accountNumber: response.data.accountNumber,
bankName: response.data.bank.name,
bankSlug: response.data.bank.slug,
transactionReference: response.data.transactionReference,
pusherChannel: response.data.pusherChannel,
accountExpiresAt: response.data.accountExpiresAt,
transactionId: response.data.transactionId)
}
}

extension BankTransferDetails {
static var example: BankTransferDetails {
BankTransferDetails(
accountName: "PAYSTACK CHECKOUT",
accountNumber: "9985488398",
bankName: "Paystack-Titan",
bankSlug: "titan-paystack",
transactionReference: "T6215047322I100043S0g703",
pusherChannel: "PWT6215047322",
accountExpiresAt: Date().addingTimeInterval(30 * 60),
transactionId: "6215047322")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

enum BankTransferProviderCatalog {

static func displayName(forSlug slug: String) -> String {
switch slug.lowercased() {
case "wema-bank":
return "Wema Bank"
case "titan-paystack":
return "Paystack-Titan"
case "paystack-mfb":
return "Paystack MFB"
default:
return titleCased(slug)
}
}

private static func titleCased(_ slug: String) -> String {
slug.split(separator: "-")
.map { segment -> String in
let lowered = segment.lowercased()
guard let first = lowered.first else { return "" }
return first.uppercased() + lowered.dropFirst()
}
.filter { !$0.isEmpty }
.joined(separator: " ")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

enum BankTransferState: Equatable {

case loading(message: String? = nil)

case awaitingPayment(BankTransferDetails)

case confirmingPayment(BankTransferDetails, phase: ConfirmingPhase)

case takingLongerThanExpected(BankTransferDetails)

case delayedConfirmation(BankTransferDetails)

case refundInitiated(BankTransferDetails, message: String)

case error(ChargeError)

case fatalError(error: ChargeError)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

enum BankTransferStatus: Equatable {
case success

case creditRequestPending

case creditRequestReceived

case creditRequestRejected

case incorrectAmountSent

case pending

case requery

case failed

case unknown(String)

init(rawStatus: String, message: String?) {

Check warning on line 22 in Sources/PaystackUI/Charge/BankTransfer/Models/BankTransferStatus.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "message" or name it "_".

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AZ7LlPLVBW70q7L2CYZQ&open=AZ7LlPLVBW70q7L2CYZQ&pullRequest=125
switch rawStatus {
case "success":
self = .success
case "transfer-credit-request-pending":
self = .creditRequestPending
case "transfer-credit-request-received":
self = .creditRequestReceived
case "transfer-credit-request-rejected":
self = .creditRequestRejected
case "incorrect-amount-sent":
self = .incorrectAmountSent
case "pending":
self = .pending
case "requery":
self = .requery
case "failed":
self = .failed
default:
self = .unknown(rawStatus)
}
}

var isTerminal: Bool {
switch self {
case .success, .creditRequestRejected, .incorrectAmountSent, .failed:
return true
case .creditRequestPending, .creditRequestReceived,
.pending, .requery, .unknown:
return false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import PaystackCore

struct BankTransferTransactionUpdate: Equatable {
let status: BankTransferStatus
let message: String?
let reference: String?
let transactionId: String?
}

extension BankTransferTransactionUpdate {

static func from(_ response: PayWithTransferPusherResponse) -> Self {
BankTransferTransactionUpdate(
status: BankTransferStatus(rawStatus: response.status,
message: response.message),
message: response.message,
reference: response.data?.reference ?? response.data?.trxref,
transactionId: response.data?.transactionId ?? response.data?.referenceId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

enum ConfirmingPhase: Equatable {

case waitingForCredit

case transferOnTheWay
}
Loading
Loading