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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
let onIveSentTheMoney: () async -> Void
let onChangePaymentMethod: () -> Void

@Environment(\.displayAudience)
private var displayAudience

@State private var provisionedAt: Date = Date()
@State private var now: Date = Date()

Expand Down Expand Up @@ -52,24 +55,28 @@
}
}

private var copyTrailing: AccountDetailRow.Trailing {
displayAudience.shows(.bankTransferCopyButtons) ? .copy : .none
}

private var accountCard: some View {
VStack(spacing: 0) {
AccountDetailRow(label: "BANK NAME",
value: details.bankName)
divider
AccountDetailRow(label: "ACCOUNT NUMBER",
value: details.accountNumber,
trailing: .copy)
trailing: copyTrailing)
divider
AccountDetailRow(label: "AMOUNT",
value: amount.description,
trailing: .copy)
trailing: copyTrailing)

if provider == .pesalink {
divider
AccountDetailRow(label: "NARRATION / REASON",
value: details.transactionReference,
trailing: .copy)
trailing: copyTrailing)
}
}
.overlay(
Expand Down Expand Up @@ -117,3 +124,28 @@
return min(1.0, elapsed / total)
}
}

@available(iOS 14.0, *)
struct BankTransferAccountDetailsView_Previews: PreviewProvider {
static var previews: some View {
Group {
BankTransferAccountDetailsView(
details: .example,
amount: AmountCurrency(amount: 1000000, currency: "NGN"),
provider: .standard,
onIveSentTheMoney: {},

Check failure on line 136 in Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AaEcNmW4FkDlAHMIetcm&open=AaEcNmW4FkDlAHMIetcm&pullRequest=137
onChangePaymentMethod: {})

Check failure on line 137 in Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AaEcNmW4FkDlAHMIetcn&open=AaEcNmW4FkDlAHMIetcn&pullRequest=137
.environment(\.displayAudience, .customerFacing)
.previewDisplayName("Customer-facing — copy buttons")

BankTransferAccountDetailsView(
details: .example,
amount: AmountCurrency(amount: 1000000, currency: "NGN"),
provider: .standard,
onIveSentTheMoney: {},

Check failure on line 145 in Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AaEcNmW4FkDlAHMIetco&open=AaEcNmW4FkDlAHMIetco&pullRequest=137
onChangePaymentMethod: {})

Check failure on line 146 in Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AaEcNmW4FkDlAHMIetcp&open=AaEcNmW4FkDlAHMIetcp&pullRequest=137
.environment(\.displayAudience, .merchantFacing)
.previewDisplayName("Merchant-facing — no copy buttons")
}
}
}
2 changes: 1 addition & 1 deletion Sources/PaystackUI/Charge/QR/Models/QRDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension QRDetails {
guard let url = URL(string: response.data.url) else { return nil }
return QRDetails(
qrImageURL: url,
qrReference: variant.showsQRReferenceRow ? response.data.qrCode : nil,
qrReference: variant.supportsAppDeeplinks ? response.data.qrCode : nil,
pusherChannel: response.data.channel)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/PaystackUI/Charge/QR/Models/QRVariant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ enum QRVariant: String, Equatable {
}
}

var showsQRReferenceRow: Bool {
var supportsAppDeeplinks: Bool {
switch self {
case .scanToPay:
return false
return true
case .snapScan:
return false
}
Expand Down
73 changes: 73 additions & 0 deletions Sources/PaystackUI/Charge/QR/Models/ScanToPayApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Foundation

struct ScanToPayApp: Equatable, Identifiable {

enum DeeplinkStyle: Equatable {
case fnb
case nedbank
case generic
}

let name: String
let iconAsset: String
let deeplinkStyle: DeeplinkStyle

var id: String { iconAsset }

var initials: String {
let words = name.split(separator: " ")
if words.count >= 2 {
return words.prefix(2).compactMap { $0.first }.map(String.init).joined().uppercased()
}
return String(name.prefix(2)).uppercased()
}

func deeplink(for qrCode: String) -> URL? {
guard let encoded = URIComponentEncoder.encode(qrCode) else { return nil }
switch deeplinkStyle {
case .fnb:
return URL(string: "https://www.online.fnb.co.za/banking/mobileservices"

Check warning on line 29 in Sources/PaystackUI/Charge/QR/Models/ScanToPayApp.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=AaEcNmXmFkDlAHMIetcq&open=AaEcNmXmFkDlAHMIetcq&pullRequest=137
+ "?codetype=masterpassdeeplink&urlencodedqr=\(encoded)")
case .nedbank:
return URL(string: "nedbank://masterpass.oltio.co.za/\(encoded)")
case .generic:
return URL(string: "masterpass.app.scheme://masterpass.oltio.co.za/\(encoded)")
}
}
}

enum ScanToPayAppDirectory {

static let apps: [ScanToPayApp] = [
ScanToPayApp(name: "Absa Scan to Pay", iconAsset: "scanToPayAppAbsa", deeplinkStyle: .generic),
ScanToPayApp(name: "Avo", iconAsset: "scanToPayAppAvo", deeplinkStyle: .generic),
ScanToPayApp(name: "Capitec", iconAsset: "scanToPayAppCapitec", deeplinkStyle: .generic),
ScanToPayApp(name: "FNB Pay", iconAsset: "scanToPayAppFNB", deeplinkStyle: .fnb),
ScanToPayApp(name: "Instapay", iconAsset: "scanToPayAppInstapay", deeplinkStyle: .generic),
ScanToPayApp(name: "Nedbank Money", iconAsset: "scanToPayAppNedbankMoney", deeplinkStyle: .nedbank),
ScanToPayApp(name: "Nedbank Scan to Pay", iconAsset: "scanToPayAppNedbankScanToPay", deeplinkStyle: .generic),
ScanToPayApp(name: "RMB", iconAsset: "scanToPayAppRMB", deeplinkStyle: .fnb),
ScanToPayApp(name: "Spot", iconAsset: "scanToPayAppSpot", deeplinkStyle: .generic),
ScanToPayApp(name: "Standard Bank Scan to Pay", iconAsset: "scanToPayAppStandardBank", deeplinkStyle: .generic),
ScanToPayApp(name: "Telkom Pay", iconAsset: "scanToPayAppTelkomPay", deeplinkStyle: .generic),
ScanToPayApp(name: "Scan to Pay", iconAsset: "scanToPayAppScanToPay", deeplinkStyle: .generic),
ScanToPayApp(name: "Vodapay", iconAsset: "scanToPayAppVodapay", deeplinkStyle: .generic)
]

static func openFailedMessage(appName: String) -> String {
"Couldn't open \(appName). Make sure it's installed."
}
}

enum URIComponentEncoder {

private static let allowed: CharacterSet = {
var set = CharacterSet()
set.insert(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()")
return set
}()

static func encode(_ value: String) -> String? {
value.addingPercentEncoding(withAllowedCharacters: allowed)
}
}
79 changes: 71 additions & 8 deletions Sources/PaystackUI/Charge/QR/Views/QRDisplayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,45 @@
let onICompletedPayment: () -> Void
let onChangePaymentMethod: () -> Void

@Environment(\.displayAudience)
private var displayAudience

@State private var showCopiedToast = false
@State private var showsAppPicker = false

var body: some View {
Group {
if showsAppPicker, let qrCode = deeplinkQRCode {
ScanToPayAppPickerView(qrCode: qrCode,
onBack: { showsAppPicker = false })
} else {
paymentDetails
}
}
.copiedToast(isPresented: $showCopiedToast)
}

private var paymentDetails: some View {
ScrollView {
VStack(spacing: .triplePadding) {

headerLogo

Text(variant.instructionCopy)
Text(instructionText)
.font(.body16M)
.foregroundColor(.contentPrimary)
.multilineTextAlignment(.center)

qrCodeBlock
if showsQRCode {
qrCodeBlock
}

Text(amount.description)
.font(.body16M)
.foregroundColor(.contentPrimary)

if variant.showsQRReferenceRow, let reference = details.qrReference {
qrReferenceRow(reference: reference)
if showsQRNumber, let qrCode = deeplinkQRCode {
qrReferenceRow(reference: qrCode)
}

if let banner = inlineBanner {
Expand All @@ -44,7 +62,33 @@
}
.padding(.doublePadding)
}
.copiedToast(isPresented: $showCopiedToast)
}

private var deeplinkQRCode: String? {
guard variant.supportsAppDeeplinks,
let qrCode = details.qrReference,
!qrCode.isEmpty else { return nil }
return qrCode
}

private var deeplinksAvailable: Bool {
deeplinkQRCode != nil
}

private var instructionText: String {
deeplinksAvailable ? displayAudience.text(.scanToPayInstruction) : variant.instructionCopy
}

private var showsQRCode: Bool {
!deeplinksAvailable || displayAudience.shows(.scanToPayQRCode)
}

private var showsQRNumber: Bool {
deeplinksAvailable && displayAudience.shows(.scanToPayQRNumber)
}

private var showsSeeAppsButton: Bool {
deeplinksAvailable && displayAudience.shows(.scanToPaySeeAppsButton)
}

private var headerLogo: some View {
Expand Down Expand Up @@ -91,8 +135,16 @@

private var actionButtons: some View {
VStack(spacing: .singlePadding) {
Button("I've completed payment", action: onICompletedPayment)
.buttonStyle(PrimaryButtonStyle(showLoading: false))
if showsSeeAppsButton {
Button("See Scan to Pay apps", action: { showsAppPicker = true })
.buttonStyle(PrimaryButtonStyle(showLoading: false))

Button("I've completed payment", action: onICompletedPayment)
.buttonStyle(SecondaryButtonStyle())
} else {
Button("I've completed payment", action: onICompletedPayment)
.buttonStyle(PrimaryButtonStyle(showLoading: false))
}

Button("Change payment method", action: onChangePaymentMethod)
.foregroundColor(.contentSecondary)
Expand Down Expand Up @@ -122,7 +174,18 @@
inlineBanner: nil,
onICompletedPayment: {},
onChangePaymentMethod: {})
.previewDisplayName("Scan to Pay — awaiting scan")
.environment(\.displayAudience, .customerFacing)
.previewDisplayName("Scan to Pay — customer-facing (number + apps)")

QRDisplayView(
variant: .scanToPay,
details: .scanToPayExample,
amount: AmountCurrency(amount: 4500, currency: "ZAR"),
inlineBanner: nil,
onICompletedPayment: {},

Check failure on line 185 in Sources/PaystackUI/Charge/QR/Views/QRDisplayView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AaEcNmZZFkDlAHMIetcr&open=AaEcNmZZFkDlAHMIetcr&pullRequest=137
onChangePaymentMethod: {})

Check failure on line 186 in Sources/PaystackUI/Charge/QR/Views/QRDisplayView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=PaystackHQ_paystack-sdk-ios&issues=AaEcNmZZFkDlAHMIetcs&open=AaEcNmZZFkDlAHMIetcs&pullRequest=137
.environment(\.displayAudience, .merchantFacing)
.previewDisplayName("Scan to Pay — merchant-facing (QR only)")

QRDisplayView(
variant: .snapScan,
Expand Down
Loading
Loading