Skip to content
Merged
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
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ let package = Package(
.copy("API/Charge/Resources/PayWithTransferPusherCreditPending.json"),
.copy("API/Charge/Resources/PayWithTransferPusherCreditRejected.json"),
.copy("API/Charge/Resources/PayWithTransferPusherIncorrectAmount.json"),
.copy("API/Charge/Resources/ZapMandateResponse.json")
.copy("API/Charge/Resources/ZapMandateResponse.json"),
.copy("API/Charge/Resources/CapitecPayAuthenticateResponse.json"),
.copy("API/Charge/Resources/QRGenerateResponse.json"),
.copy("API/Charge/Resources/ZapPusherSuccess.json"),
.copy("API/Charge/Resources/ZapPusherFailed.json"),
.copy("API/Charge/Resources/CapitecPayPusherSuccess.json"),
.copy("API/Charge/Resources/CapitecPayPusherFailed.json"),
.copy("API/Charge/Resources/QRPusherSuccess.json"),
.copy("API/Charge/Resources/QRPusherFailed.json")

])
]
Expand Down
18 changes: 9 additions & 9 deletions Sources/PaystackSDK/API/Charge/Zap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ public extension Paystack {
}

/// Listens for Zap status updates on the Pusher channel returned by
/// ``initiateZapMandate(_:)``. The status taxonomy is shared with
/// Pay-with-Transfer, so this
/// helper returns the existing `PayWithTransferPusherResponse` shape.
/// ``initiateZapMandate(_:)``. The server publishes only terminal
/// events (`success` / `failed`) on the Zap 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 ; callers that need to keep
/// listening through transient statuses must re-subscribe after each
/// event.
/// `PusherSubscriptionListener` contract — one event resolves the
/// listener.
///
/// - Parameter channelName: The `pusherChannel` value returned from
/// `initiateZapMandate` (e.g. `DBMAN_6222375579`).
/// - Returns: A ``Service`` carrying a ``PayWithTransferPusherResponse``
/// on the first event the channel emits.
/// - Returns: A ``Service`` carrying a ``Charge3DSResponse`` on the
/// first event the channel emits.
func listenForZapResponse(onChannel channelName: String)
-> Service<PayWithTransferPusherResponse> {
-> Service<Charge3DSResponse> {
let subscription: any Subscription = PusherSubscription(
channelName: channelName, eventName: "response")
return Service(subscription)
Expand Down
5 changes: 5 additions & 0 deletions Sources/PaystackSDK/Core/Models/Models/EmptyRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public struct EmptyRequest: Encodable, Equatable {
public init() {}

Check failure on line 4 in Sources/PaystackSDK/Core/Models/Models/EmptyRequest.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AZ9HdhFMwnzmLlEyVM9K&open=AZ9HdhFMwnzmLlEyVM9K&pullRequest=131
}
22 changes: 21 additions & 1 deletion Sources/PaystackSDK/Core/Utils/Cryptography/Cryptography.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import Foundation

struct Cryptography {
public protocol CryptographyProtocol {
func encryptPKCS1(text: String, publicKey: String) throws -> String
}

public struct Cryptography: CryptographyProtocol {

public init() {}

Check failure on line 9 in Sources/PaystackSDK/Core/Utils/Cryptography/Cryptography.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AZ9HdhF8wnzmLlEyVM9L&open=AZ9HdhF8wnzmLlEyVM9L&pullRequest=131

func encrypt(text: String, publicKey: String) throws -> String {
let key = try createKey(from: publicKey, isPublic: true)
Expand All @@ -21,6 +27,20 @@
}
return try encrypt(text: jsonString, publicKey: publicKey)
}

/// RSA-encrypts `text` with PKCS#1 v1.5 padding and base64-encodes the result.
/// Mirrors Node's `crypto.publicEncrypt` with `constants.RSA_PKCS1_PADDING`.
public func encryptPKCS1(text: String, publicKey: String) throws -> String {
let key = try createKey(from: publicKey, isPublic: true)

var encryptionError: Unmanaged<CFError>?
guard let textData = text.data(using: .utf8),
let encryptedData = SecKeyCreateEncryptedData(key, .rsaEncryptionPKCS1,
textData as CFData, &encryptionError) as Data? else {
throw CryptographyError.encryptionFailed
}
return encryptedData.base64EncodedString()
}
}

// MARK: - Preparing Key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ protocol ZapRepository {
walletEmail: String) async throws -> ZapMandateResponse

func listenForZapResponse(onChannel channelName: String)
async throws -> BankTransferTransactionUpdate
async throws -> ChargeCardTransaction
}

struct ZapRepositoryImplementation: ZapRepository {
Expand All @@ -29,9 +29,9 @@ struct ZapRepositoryImplementation: ZapRepository {
}

func listenForZapResponse(onChannel channelName: String)
async throws -> BankTransferTransactionUpdate {
async throws -> ChargeCardTransaction {
let response = try await paystack
.listenForZapResponse(onChannel: channelName).async()
return BankTransferTransactionUpdate.from(response)
return ChargeCardTransaction.from(response)
}
}
21 changes: 7 additions & 14 deletions Sources/PaystackUI/Charge/Zap/Viewmodels/ZapViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,33 +97,26 @@ class ZapViewModel: ObservableObject {
}
}

// MARK: - Pusher listen loop
// MARK: - Pusher (single-shot)

private func startListeningForPusher(on details: ZapDetails) {
pusherTask?.cancel()
let channel = details.pusherChannel
pusherTask = Task { [weak self] in
await self?.listenLoop(on: channel)
}
}

private func listenLoop(on channel: String) async {
while !Task.isCancelled {
guard let self else { return }
do {
let update = try await repository
let update = try await self.repository
.listenForZapResponse(onChannel: channel)
await processTransactionUpdate(update)
if update.status.isTerminal { return }
await self.processTransactionUpdate(update)
} catch {
Logger.error("Zap Pusher iteration failed: %@",
Logger.error("Zap Pusher await failed: %@",
arguments: error.localizedDescription)
return
}
}
}

@MainActor
func processTransactionUpdate(_ update: BankTransferTransactionUpdate) async {
func processTransactionUpdate(_ update: ChargeCardTransaction) async {
switch update.status {

case .success:
Expand All @@ -136,7 +129,7 @@ class ZapViewModel: ObservableObject {
state = .error(ChargeError(message: message))

default:
Logger.info("Zap: unexpected Pusher status %@",
Logger.info("Zap: non-terminal transaction status %@",
arguments: String(describing: update.status))
}
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "CapitecPayMark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "scan-to-pay_logo-80-UG7U5qv_.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "snapscan_logo-BYsTYDYq.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "1f1ff-1f1e6.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"status": true,
"type": "success",
"code": "ok",
"data": {
"status": "success",
"timeToLive": 120,
"expiryDate": "2026-07-07T12:29:20.000Z"
},
"message": "Charge pending"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"status": "failed",
"trans": "5900549926",
"trxref": "T_ref_5900549926",
"message": "Bank declined"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"status": "success",
"trans": "5900549926",
"trxref": "T_ref_5900549926",
"reference": "T_ref_5900549926",
"message": "Payment approved"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"status": true,
"message": "QR successfully generated",
"data": {
"errors": false,
"url": "https://s3.eu-west-1.amazonaws.com/files.paystack.co/qr/mpass_olti/518262/1783510266603.png?X-Amz-Signature=abc",
"qr_code": "1490884538",
"status": "success",
"channel": "api_mpass_olti_qr_51826223921246"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"status": "failed",
"trans": "5900549926",
"trxref": "T_qr_5900549926",
"message": "Wallet declined the payment"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"status": "success",
"trans": "5900549926",
"trxref": "T_qr_5900549926",
"reference": "T_qr_5900549926",
"message": "Payment received"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"status": "failed",
"trans": "6222375579",
"trxref": "T_zap_6222375579",
"message": "Bank declined the mandate"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"status": "success",
"trans": "6222375579",
"trxref": "T_zap_6222375579",
"reference": "T_zap_6222375579",
"message": "Payment successful"
}
17 changes: 15 additions & 2 deletions Tests/PaystackSDKTests/API/Charge/ZapTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,24 @@ final class ZapTests: PSTestCase {
let channelName = "DBMAN_6222375579"
mockSubscriptionListener
.expectSubscription(PusherSubscription(channelName: channelName, eventName: "response"))
.andReturnString(fromJson: "PayWithTransferPusherSuccess")
.andReturnString(fromJson: "ZapPusherSuccess")

let result = try await serviceUnderTest
.listenForZapResponse(onChannel: channelName).async()

XCTAssertEqual(result.status, "success")
XCTAssertEqual(result.status, .success)
}

func testListenForZapResponseDecodesFailedShape() async throws {
let channelName = "DBMAN_6222375579"
mockSubscriptionListener
.expectSubscription(PusherSubscription(channelName: channelName, eventName: "response"))
.andReturnString(fromJson: "ZapPusherFailed")

let result = try await serviceUnderTest
.listenForZapResponse(onChannel: channelName).async()

XCTAssertEqual(result.status, .failed)
XCTAssertEqual(result.message, "Bank declined the mandate")
}
}
Loading
Loading