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
61 changes: 61 additions & 0 deletions Sources/PaystackSDK/API/Charge/CapitecPay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation

/// Public Capitec Pay surface. Used by the UI module to authenticate a
/// Capitec Pay transaction, poll for its status via the Capitec-specific
/// requery endpoint, and subscribe for Pusher events. Can also be called
/// directly by integrators driving their own UI on top of `PaystackCore`.
public extension Paystack {

private var capitecPayService: CapitecPayService {
return CapitecPayServiceImplementation(config: config)
}

/// Authenticates a Capitec Pay transaction against
/// `POST /capitec-pay/authenticate`. The `clientdata` field on
/// ``CapitecPayAuthenticateRequest`` must already be RSA-encrypted
/// using the merchant's public encryption key from
/// ``VerifyAccessCode``.
///
/// - Parameter request: The authenticate payload — pre-encrypted
/// client data, transaction id, and device fingerprint.
/// - Returns: A ``Service`` carrying a ``CapitecPayAuthenticateResponse``
/// with the `timeToLive` window the SDK counts down against.
func authenticateCapitecPay(_ request: CapitecPayAuthenticateRequest)
-> Service<CapitecPayAuthenticateResponse> {
return capitecPayService.postAuthenticate(request)
}

/// Polls the Capitec Pay requery endpoint
/// (`POST /capitec-pay/requery/{transactionReference}`) for the
/// current transaction status. The response follows the standard
/// charge shape — same decoding path as ``checkPendingCharge(forAccessCode:)``.
///
/// - Parameter transactionReference: The `reference` returned from
/// `verify_access_code`.
/// - Returns: A ``Service`` carrying a ``ChargeResponse``.
func requeryCapitecPay(transactionReference: String)
-> Service<ChargeResponse> {
return capitecPayService.postRequery(transactionReference: transactionReference)
}

/// Listens for Capitec Pay status updates on the Pusher channel
/// returned by ``authenticateCapitecPay(_:)``. The server publishes
/// only terminal events (`success` / `failed`) on the Capitec Pay
/// channel, so this helper returns the narrow ``Charge3DSResponse``
/// shape shared with card 3-D Secure and mobile money authorization.
///
/// The underlying listener is single-shot per the existing
/// `PusherSubscriptionListener` contract — one event resolves the
/// listener.
///
/// - Parameter channelName: The `CAPITECPAY_{transactionId}` channel
/// for this transaction.
/// - Returns: A ``Service`` carrying a ``Charge3DSResponse`` on the
/// first event the channel emits.
func listenForCapitecPayResponse(onChannel channelName: String)
-> Service<Charge3DSResponse> {
let subscription: any Subscription = PusherSubscription(
channelName: channelName, eventName: "response")
return Service(subscription)
}
}
27 changes: 27 additions & 0 deletions Sources/PaystackSDK/API/Charge/CapitecPayService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

protocol CapitecPayService: PaystackService {
func postAuthenticate(_ request: CapitecPayAuthenticateRequest)
-> Service<CapitecPayAuthenticateResponse>
func postRequery(transactionReference: String)
-> Service<ChargeResponse>
}

struct CapitecPayServiceImplementation: CapitecPayService {

var config: PaystackConfig

var parentPath: String { "capitec-pay" }

func postAuthenticate(_ request: CapitecPayAuthenticateRequest)
-> Service<CapitecPayAuthenticateResponse> {
return post("/authenticate", request)

Check warning on line 18 in Sources/PaystackSDK/API/Charge/CapitecPayService.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=AZ-NqO3QzkWu0uAdOynS&open=AZ-NqO3QzkWu0uAdOynS&pullRequest=132
.asService()
}

func postRequery(transactionReference: String)
-> Service<ChargeResponse> {
return post("/requery/\(transactionReference)", EmptyRequest())
.asService()
}
}
52 changes: 52 additions & 0 deletions Sources/PaystackSDK/API/Charge/QR.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

/// Public QR-payment surface. Used by the UI module to generate a hosted
/// QR image for a transaction (Scan to Pay / Snap Scan et al) and to
/// subscribe for Pusher status events. Can also be called directly by
/// integrators driving their own UI on top of `PaystackCore`.
public extension Paystack {

private var qrService: QRService {
return QRServiceImplementation(config: config)
}

/// Generates a QR code for a transaction against
/// `POST /offline/qr/generate`. The `channel` field on
/// ``QRGenerateRequest`` is the provider code returned in
/// `VerifyAccessCode.channelOptions.qrCode` (for example
/// `"MPASS_OLTI"` for Ukheshe-served flows in ZA) — the SDK does not
/// hard-code that value so the same call works for future markets
/// that expose different provider codes.
///
/// - Parameter request: The generate payload — source, transaction
/// reference, and provider channel code.
/// - Returns: A ``Service`` carrying a ``QRGenerateResponse`` whose
/// `data.url` is a signed S3 URL for the QR image and whose
/// `data.channel` is the Pusher channel to subscribe on.
func generateQR(_ request: QRGenerateRequest)
-> Service<QRGenerateResponse> {
return qrService.postGenerate(request)
}

/// Listens for QR-payment status updates on the Pusher channel
/// returned by ``generateQR(_:)``. The server publishes only terminal
/// events (`success` / `failed`) on the QR channel, so this helper
/// returns the narrow ``Charge3DSResponse`` shape shared with card
/// 3-D Secure and mobile money authorization.
///
/// The underlying listener is single-shot per the existing
/// `PusherSubscriptionListener` contract — one event resolves the
/// listener.
///
/// - Parameter channelName: The `data.channel` value returned by
/// ``generateQR(_:)`` (for example
/// `"api_mpass_olti_qr_51826223921246"`).
/// - Returns: A ``Service`` carrying a ``Charge3DSResponse`` on the
/// first event the channel emits.
func listenForQRResponse(onChannel channelName: String)
-> Service<Charge3DSResponse> {
let subscription: any Subscription = PusherSubscription(
channelName: channelName, eventName: "response")
return Service(subscription)
}
}
19 changes: 19 additions & 0 deletions Sources/PaystackSDK/API/Charge/QRService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

protocol QRService: PaystackService {
func postGenerate(_ request: QRGenerateRequest)
-> Service<QRGenerateResponse>
}

struct QRServiceImplementation: QRService {

var config: PaystackConfig

var parentPath: String { "offline/qr" }

func postGenerate(_ request: QRGenerateRequest)
-> Service<QRGenerateResponse> {
return post("/generate", request)

Check warning on line 16 in Sources/PaystackSDK/API/Charge/QRService.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=AZ-NqO3HzkWu0uAdOynR&open=AZ-NqO3HzkWu0uAdOynR&pullRequest=132
.asService()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

public struct CapitecPayAuthenticateRequest: Encodable, Equatable {
public let clientdata: String
public let trans: String
public let device: String

public init(clientdata: String, trans: String, device: String) {
self.clientdata = clientdata
self.trans = trans
self.device = device
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

public struct CapitecPayAuthenticateResponse: Decodable, Equatable {
public let status: Bool
public let type: String
public let code: String
public let data: CapitecPayAuthenticateData
public let message: String

public init(status: Bool,
type: String,
code: String,
data: CapitecPayAuthenticateData,
message: String) {
self.status = status
self.type = type
self.code = code
self.data = data
self.message = message
}
}

public struct CapitecPayAuthenticateData: Decodable, Equatable {
public let status: String
public let timeToLive: Int
public let expiryDate: Date

public init(status: String, timeToLive: Int, expiryDate: Date) {
self.status = status
self.timeToLive = timeToLive
self.expiryDate = expiryDate
}
}
1 change: 1 addition & 0 deletions Sources/PaystackSDK/Core/Models/Models/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum Channel: String, Codable {
case mobileMoney = "mobile_money"
case qr = "qr"
case bankTransfer = "bank_transfer"
case capitecPay = "capitec_pay"
case unsupportedChannel

public init(from decoder: Decoder) throws {
Expand Down
15 changes: 15 additions & 0 deletions Sources/PaystackSDK/Core/Models/Models/QRGenerateRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

public struct QRGenerateRequest: Encodable, Equatable {
public let source: String
public let reference: String
public let channel: String

public init(source: String = "mobile-pos",
reference: String,
channel: String) {
self.source = source
self.reference = reference
self.channel = channel
}
}
33 changes: 33 additions & 0 deletions Sources/PaystackSDK/Core/Models/Models/QRGenerateResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

public struct QRGenerateResponse: Decodable, Equatable {
public let status: Bool
public let message: String
public let data: QRGenerateData

public init(status: Bool, message: String, data: QRGenerateData) {
self.status = status
self.message = message
self.data = data
}
}

public struct QRGenerateData: Decodable, Equatable {
public let errors: Bool
public let url: String
public let qrCode: String
public let status: String
public let channel: String

public init(errors: Bool,
url: String,
qrCode: String,
status: String,
channel: String) {
self.errors = errors
self.url = url
self.qrCode = qrCode
self.status = status
self.channel = channel
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

struct CapitecPayConfig: Equatable {
let transactionId: Int
let transactionReference: String
let publicEncryptionKey: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation
import PaystackCore

struct CapitecPayDetails: Equatable {
let timeToLive: Int
let expiryDate: Date
let pusherChannel: String
}

extension CapitecPayDetails {
static func from(_ response: CapitecPayAuthenticateResponse,
transactionId: Int) -> CapitecPayDetails {
CapitecPayDetails(
timeToLive: response.data.timeToLive,
expiryDate: response.data.expiryDate,
pusherChannel: "CAPITECPAY_\(transactionId)")
}
}

extension CapitecPayDetails {
static var example: CapitecPayDetails {
CapitecPayDetails(
timeToLive: 120,
expiryDate: Date().addingTimeInterval(120),
pusherChannel: "CAPITECPAY_5900549926")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

enum CapitecPayIdentifier: String, CaseIterable, Equatable, CustomStringConvertible {
case cellphone = "CELLPHONE"
case idNumber = "IDNUMBER"
case accountNumber = "ACCOUNTNUMBER"

var pickerTitle: String {
switch self {
case .cellphone: return "Cellphone Number"
case .idNumber: return "ID Number"
case .accountNumber: return "Account Number"
}
}

var description: String { pickerTitle }

var prompt: String {
switch self {
case .cellphone:
return "Enter the mobile number linked to your Capitec account"
case .idNumber:
return "Enter the ID number linked to your Capitec account"
case .accountNumber:
return "Enter the account number linked to your Capitec account"
}
}

var placeholder: String {
switch self {
case .cellphone: return "Enter your cellphone number"
case .idNumber: return "Enter your ID number"
case .accountNumber: return "Enter your account number"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

enum CapitecPayState: Equatable {
case identifierEntry
case authenticating
case awaitingApproval(CapitecPayDetails)
case requerying(CapitecPayDetails)
case error(ChargeError)
case fatalError(error: ChargeError)
}
Loading
Loading