-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathChargeViewModel.swift
More file actions
140 lines (114 loc) · 4.55 KB
/
Copy pathChargeViewModel.swift
File metadata and controls
140 lines (114 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import Foundation
import SwiftUI
import PaystackCore
class ChargeViewModel: ObservableObject {
let accessCode: String
let repository: ChargeRepository
@Published
var transactionState: ChargeState = .loading()
var transactionDetails: VerifyAccessCode?
init(accessCode: String,
repository: ChargeRepository = ChargeRepositoryImplementation()) {
self.accessCode = accessCode
self.repository = repository
}
@MainActor
func verifyAccessCodeAndProceed() async {
do {
transactionState = .loading()
let response = try await repository.verifyAccessCode(accessCode)
let supported = resolveSupportedChannels(from: response)
guard !supported.isEmpty else {
let message = "No supported payment methods. " +
"Please reach out to your merchant for further information"
let cause = "No payment channels on this integration " +
"are supported by the SDK"
throw ChargeError(displayMessage: message, causeMessage: cause)
}
transactionDetails = response
transactionState = nextState(for: supported, response: response)
} catch {
let error = ChargeError(error: error)
Logger.error("Verify access code failed with error: %@",
arguments: error.cause?.localizedDescription ?? "Unknown")
// TODO: Display a fatal error here in the future instead
transactionState = .error(error)
}
}
private func resolveSupportedChannels(from response: VerifyAccessCode) -> [SupportedChannel] {
var result: [SupportedChannel] = []
if response.paymentChannels.contains(.card) {
result.append(.card)
}
if response.paymentChannels.contains(.mobileMoney),
let providers = response.channelOptions?.mobileMoney, !providers.isEmpty {
let allowed = filtered(providers)
result.append(contentsOf: allowed.map { .mobileMoney($0) })
}
return result
}
private func filtered(_ providers: [MobileMoneyChannel]) -> [MobileMoneyChannel] {
guard let allowlist = Self.supportedMobileMoneyProviders else {
return providers
}
return providers.filter { allowlist.contains($0.key.uppercased()) }
}
private func nextState(for channels: [SupportedChannel],
response: VerifyAccessCode) -> ChargeState {
if channels.count == 1, case .card = channels[0] {
return .payment(type: .card(transactionInformation: response))
}
if !channels.contains(.card),
channels.count == 1,
case .mobileMoney(let provider) = channels[0] {
return .payment(type: .mobileMoney(transactionInformation: response,
provider: provider))
}
return .channelSelection(transactionInformation: response,
supportedChannels: channels)
}
}
// MARK: - Mobile money provider allowlist
extension ChargeViewModel {
/// Mobile money provider keys (`MobileMoneyChannel.key`, uppercased) that
/// the SDK is allowed to route to. The API may return providers we don't
/// yet have logos, copy, or phone formatters for — listing them here is
/// what opts them into the UI.
///
/// Set to `nil` to accept every provider the API returns (no filtering).
/// Add a new key here when you've added its logo to `SupportedChannel.image`
/// and its country code / phone formatter to the relevant helpers.
static var supportedMobileMoneyProviders: Set<String>? = [
"MPESA"
]
}
// MARK: - Charge Container
extension ChargeViewModel: ChargeContainer {
func processSuccessfulTransaction(details: VerifyAccessCode) {
transactionState = .success(amount: details.amountCurrency,
merchant: details.merchantName,
details: .init(reference: details.reference))
}
}
// MARK: UI State Management
extension ChargeViewModel {
var centerView: Bool {
switch transactionState {
case .success:
return true
default:
return false
}
}
var displayCloseButtonConfirmation: Bool {
switch transactionState {
case .success:
return false
default:
return true
}
}
var inTestMode: Bool {
transactionDetails?.domain == .test
}
}