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
4 changes: 4 additions & 0 deletions Sources/PaystackUI/Charge/ChargeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ struct ChargeView: View {
BankTransferView(chargeContainer: viewModel,
transactionDetails: transactionInformation,
config: config)
case .zap(let transactionInformation, let config):
ZapView(chargeContainer: viewModel,
transactionDetails: transactionInformation,
config: config)
}
}

Expand Down
19 changes: 19 additions & 0 deletions Sources/PaystackUI/Charge/ChargeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ class ChargeViewModel: ObservableObject {
result.append(.bankTransfer(config))
}

if response.paymentChannels.contains(.bank),
let banks = response.supportedBanks,
let zap = banks.first(where: { Self.promotedSupportedBankCodes.contains($0.code) }),
let transactionId = response.transactionId {
let config = ZapConfig(supportedBankId: zap.id,
transactionId: transactionId,
walletEmail: response.email)
result.append(.zap(config))
}

return result
}

Expand Down Expand Up @@ -96,6 +106,13 @@ class ChargeViewModel: ObservableObject {
config: config))
}

if !channels.contains(.card),
channels.count == 1,
case .zap(let config) = channels[0] {
return .payment(type: .zap(transactionInformation: response,
config: config))
}

return .channelSelection(transactionInformation: response,
supportedChannels: channels)
}
Expand All @@ -107,6 +124,8 @@ extension ChargeViewModel {
static var supportedMobileMoneyProviders: Set<String>? = [
"MPESA", "ATL_KE", "MTN", "ATL", "VOD", "WAVE_CI", "ORANGE_CI", "MTN_CI"
]

static var promotedSupportedBankCodes: Set<String> = ["00zap"]
}

extension ChargeViewModel: ChargeContainer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class ChannelSelectionViewModel: ObservableObject {
case .bankTransfer(let config):
state = .payment(type: .bankTransfer(transactionInformation: self.information,
config: config))
case .zap(let config):
state = .payment(type: .zap(transactionInformation: self.information,
config: config))
}
}
}
Expand All @@ -70,12 +73,18 @@ struct ChannelView: View {
let channelTitle: String
let image: Image

private let imageSlotSize: CGFloat = 20

var body: some View {

HStack(spacing: .singlePadding) {

VStack(alignment: .leading, spacing: .singlePadding) {
image.frame(width: 20, height: 20)
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: imageSlotSize, height: imageSlotSize)

Text(channelTitle)
.font(.body14M)
.foregroundColor(.navy02)
Expand Down
2 changes: 2 additions & 0 deletions Sources/PaystackUI/Charge/Models/ChargePaymentType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ enum ChargePaymentType: Equatable {
provider: MobileMoneyChannel)
case bankTransfer(transactionInformation: VerifyAccessCode,
config: BankTransferConfig)
case zap(transactionInformation: VerifyAccessCode,
config: ZapConfig)
}
7 changes: 7 additions & 0 deletions Sources/PaystackUI/Charge/Models/SupportedChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum SupportedChannel: Equatable, Identifiable {
case card
case mobileMoney(MobileMoneyChannel)
case bankTransfer(BankTransferConfig)
case zap(ZapConfig)

var id: String {
switch self {
Expand All @@ -14,6 +15,8 @@ enum SupportedChannel: Equatable, Identifiable {
return "mobile_money.\(channel.key)"
case .bankTransfer:
return "bank_transfer"
case .zap:
return "zap"
}
}

Expand All @@ -25,6 +28,8 @@ enum SupportedChannel: Equatable, Identifiable {
return channel.value
case .bankTransfer:
return "Transfer"
case .zap:
return "Zap"
}
}

Expand All @@ -36,6 +41,8 @@ enum SupportedChannel: Equatable, Identifiable {
return Self.image(forMobileMoneyKey: channel.key)
case .bankTransfer:
return Image(systemName: "building.columns")
case .zap:
return Image("zapSingleLogo", bundle: .current)
}
}

Expand Down
16 changes: 12 additions & 4 deletions Sources/PaystackUI/Charge/Models/VerifyAccessCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation
import PaystackCore

struct VerifyAccessCode: Equatable {

var email: String = ""
var amount: Decimal
var currency: String
var accessCode: String
Expand All @@ -14,6 +16,8 @@ struct VerifyAccessCode: Equatable {
var channelOptions: PaystackUI.ChannelOptions?
var merchantChannelSettings: MerchantChannelSettings?

var supportedBanks: [SupportedBank]?

var amountCurrency: AmountCurrency {
AmountCurrency(amount: amount, currency: currency)
}
Expand All @@ -22,7 +26,8 @@ struct VerifyAccessCode: Equatable {
extension VerifyAccessCode {

static func from(_ response: VerifyAccessCodeResponse) -> Self {
VerifyAccessCode(amount: response.data.amount,
VerifyAccessCode(email: response.data.email,
amount: response.data.amount,
currency: response.data.currency,
accessCode: response.data.accessCode,
paymentChannels: response.data.channels.filter { $0 != .unsupportedChannel },
Expand All @@ -32,20 +37,23 @@ extension VerifyAccessCode {
reference: response.data.reference,
transactionId: response.data.id,
channelOptions: PaystackUI.ChannelOptions.from(response.data.channelOptions),
merchantChannelSettings: response.data.merchantChannelSettings)
merchantChannelSettings: response.data.merchantChannelSettings,
supportedBanks: response.data.supportedBanks)
}

}

extension VerifyAccessCode {
static var example: Self {
.init(amount: 10000,
.init(email: "test@email.com",
amount: 10000,
currency: "USD",
accessCode: "test_access",
paymentChannels: [.card],
domain: .test,
merchantName: "Test Merchant",
publicEncryptionKey: "test_encryption_key",
reference: "test_reference", channelOptions: PaystackUI.ChannelOptions.example)
reference: "test_reference",
channelOptions: PaystackUI.ChannelOptions.example)
}
}
89 changes: 89 additions & 0 deletions Sources/PaystackUI/Charge/Zap/Components/QRCodeImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import SwiftUI
import PaystackCore
#if canImport(UIKit)
import UIKit
#endif

@available(iOS 14.0, *)
struct QRCodeImage: View {

let url: URL

#if canImport(UIKit)
@State private var image: UIImage?
#endif
@State private var loadFailed = false

var body: some View {
ZStack {
#if canImport(UIKit)
if let image = image {
Image(uiImage: image)
.resizable()
.interpolation(.none)
.scaledToFit()
.transition(.opacity)
} else if loadFailed {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.warning02)
} else {
ShimmerPlaceholder()
}
#else
ShimmerPlaceholder()
#endif
}
.task { await load() }
}

private func load() async {
#if canImport(UIKit)
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let loaded = UIImage(data: data) {
await MainActor.run {
withAnimation(.easeInOut(duration: 0.25)) {
self.image = loaded
}
}
} else {
await MainActor.run { self.loadFailed = true }
}
} catch {
PaystackCore.Logger.error("Zap QR load failed: %@",
arguments: error.localizedDescription)
await MainActor.run { self.loadFailed = true }
}
#endif
}
}

@available(iOS 14.0, *)
struct ShimmerPlaceholder: View {

@State private var phase: CGFloat = -1.0

var body: some View {
GeometryReader { geo in
ZStack {
Color.gray01
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color.white.opacity(0.0), location: 0.0),
.init(color: Color.white.opacity(0.5), location: 0.5),
.init(color: Color.white.opacity(0.0), location: 1.0)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing)
.frame(width: geo.size.width * 1.5)
.offset(x: phase * geo.size.width)
}
.cornerRadius(.cornerRadius)
.onAppear {
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
phase = 1.0
}
}
}
}
}
8 changes: 8 additions & 0 deletions Sources/PaystackUI/Charge/Zap/Models/ZapConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation
import PaystackCore

struct ZapConfig: Equatable {
let supportedBankId: Int
let transactionId: Int
let walletEmail: String
}
35 changes: 35 additions & 0 deletions Sources/PaystackUI/Charge/Zap/Models/ZapDetails.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import PaystackCore

struct ZapDetails: Equatable {
let qrImageURL: URL
let paymentURL: URL
let pusherChannel: String
let expiresAt: Date
}

extension ZapDetails {

static func from(_ response: ZapMandateResponse,
mandateWindowSeconds: Int) -> ZapDetails? {
guard let qr = URL(string: response.qrImage),
let payment = URL(string: response.paymentUrl) else {
return nil
}
return ZapDetails(
qrImageURL: qr,
paymentURL: payment,
pusherChannel: response.pusherChannel,
expiresAt: Date().addingTimeInterval(TimeInterval(mandateWindowSeconds)))
}
}

extension ZapDetails {
static var example: ZapDetails {
ZapDetails(
qrImageURL: URL(string: "https://example.com/qr.png")!,

Check warning on line 30 in Sources/PaystackUI/Charge/Zap/Models/ZapDetails.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=AZ75XPr5rYbXM-3DEoNG&open=AZ75XPr5rYbXM-3DEoNG&pullRequest=127
paymentURL: URL(string: "https://joinzap.com/app/merchant-payment/test")!,

Check warning on line 31 in Sources/PaystackUI/Charge/Zap/Models/ZapDetails.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=AZ75XPr5rYbXM-3DEoNH&open=AZ75XPr5rYbXM-3DEoNH&pullRequest=127
pusherChannel: "DBMAN_6222375579",
expiresAt: Date().addingTimeInterval(5 * 60))
}
}
14 changes: 14 additions & 0 deletions Sources/PaystackUI/Charge/Zap/Models/ZapState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

enum ZapState: Equatable {

case loading(message: String? = nil)

case awaitingPayment(ZapDetails)

case sessionExpired

case error(ChargeError)

case fatalError(error: ChargeError)
}
37 changes: 37 additions & 0 deletions Sources/PaystackUI/Charge/Zap/Repository/ZapRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import PaystackCore

protocol ZapRepository {

func initiateZapMandate(supportedBankId: Int,
transactionId: Int,
walletEmail: String) async throws -> ZapMandateResponse

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

struct ZapRepositoryImplementation: ZapRepository {

let paystack: Paystack

init() {
self.paystack = PaystackContainer.instance.retrieve()
}

func initiateZapMandate(supportedBankId: Int,
transactionId: Int,
walletEmail: String) async throws -> ZapMandateResponse {
let request = ZapMandateRequest(id: supportedBankId,
transactionId: transactionId,
walletId: walletEmail)
return try await paystack.initiateZapMandate(request).async()
}

func listenForZapResponse(onChannel channelName: String)
async throws -> BankTransferTransactionUpdate {
let response = try await paystack
.listenForZapResponse(onChannel: channelName).async()
return BankTransferTransactionUpdate.from(response)
}
}
Loading
Loading