From ee79b6e8d353ddc2289664776d24bfcd3d529404 Mon Sep 17 00:00:00 2001 From: Peter-John Welcome Date: Wed, 20 May 2026 14:08:29 +0200 Subject: [PATCH 1/2] Generalize M-Pesa flow into multi-provider mobile money support Rename MPesa* types/files to MobileMoney* and add a MobileMoneyFlowFactory so additional providers can plug into the same charge flow. Adds provider-aware UI helpers (expected country code, flag accessory) on MobileMoneyChannel, a Ghana flag asset, and accompanying tests. --- Sources/PaystackUI/Charge/ChargeView.swift | 6 +- .../PaystackUI/Charge/ChargeViewModel.swift | 2 +- .../MobileMoney/MobileMoneyFlowFactory.swift | 15 ++++ .../Models/MobileMoneyChannel.swift | 30 +++++++ .../ChargeMobileMoneyRepository.swift | 4 +- ...swift => MobileMoneyChargeViewModel.swift} | 23 +++-- ...t => MobileMoneyProcessingViewModel.swift} | 23 +++-- ...View.swift => MobileMoneyChargeView.swift} | 15 ++-- ....swift => MobileMoneyProcessingView.swift} | 12 +-- .../Charge/Models/SupportedChannel.swift | 4 +- Sources/PaystackUI/Images/Images.swift | 5 ++ .../ghanaFlagLogo.imageset/Contents.json | 21 +++++ .../ghanaFlagLogo.imageset/Flags.png | Bin 0 -> 421 bytes .../PaystackUI/Utils/StringExtensions.swift | 12 ++- .../MobileMoneyChannelTests.swift | 84 ++++++++++++++++++ .../MobileMoneyChargeViewModelTests.swift} | 70 ++++++++++----- ...MobileMoneyProcessingViewModelTests.swift} | 72 ++++++++++----- ...leMoneyRepositoryImplementationTests.swift | 8 +- .../MockChargeMobileMoneyRepository.swift | 6 +- ...r.swift => MockMobileMoneyContainer.swift} | 9 +- .../UI/Utils/StringExtensionsTests.swift | 51 ++++++++--- 21 files changed, 368 insertions(+), 104 deletions(-) create mode 100644 Sources/PaystackUI/Charge/MobileMoney/MobileMoneyFlowFactory.swift rename Sources/PaystackUI/Charge/MobileMoney/Viewmodels/{MPesaChrageViewModel.swift => MobileMoneyChargeViewModel.swift} (77%) rename Sources/PaystackUI/Charge/MobileMoney/Viewmodels/{MPesaProcessingViewModel.swift => MobileMoneyProcessingViewModel.swift} (69%) rename Sources/PaystackUI/Charge/MobileMoney/Views/{MPesaChargeView.swift => MobileMoneyChargeView.swift} (83%) rename Sources/PaystackUI/Charge/MobileMoney/Views/{MPesaProcessingView.swift => MobileMoneyProcessingView.swift} (91%) create mode 100644 Sources/PaystackUI/Images/Images.xcassets/ghanaFlagLogo.imageset/Contents.json create mode 100644 Sources/PaystackUI/Images/Images.xcassets/ghanaFlagLogo.imageset/Flags.png create mode 100644 Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift rename Tests/PaystackSDKTests/UI/Charge/{MPesaCharge/MPesaChrageViewModelTests.swift => MobileMoneyCharge/MobileMoneyChargeViewModelTests.swift} (73%) rename Tests/PaystackSDKTests/UI/Charge/{MPesaProcessing/MPesaProcessingViewModelTests.swift => MobileMoneyProcessing/MobileMoneyProcessingViewModelTests.swift} (52%) rename Tests/PaystackSDKTests/UI/Charge/Mocks/{MockMPesaContainer.swift => MockMobileMoneyContainer.swift} (72%) diff --git a/Sources/PaystackUI/Charge/ChargeView.swift b/Sources/PaystackUI/Charge/ChargeView.swift index abb5f4d..749ccfa 100644 --- a/Sources/PaystackUI/Charge/ChargeView.swift +++ b/Sources/PaystackUI/Charge/ChargeView.swift @@ -60,9 +60,9 @@ struct ChargeView: View { ChargeCardView(transactionDetails: transactionInformation, chargeContainer: viewModel) case .mobileMoney(let transactionInformation, let provider): - MPesaChargeView(chargeCardContainer: viewModel, - transactionDetails: transactionInformation, - provider: provider) + MobileMoneyFlowFactory.view(for: provider, + chargeContainer: viewModel, + transactionDetails: transactionInformation) } } diff --git a/Sources/PaystackUI/Charge/ChargeViewModel.swift b/Sources/PaystackUI/Charge/ChargeViewModel.swift index 9097020..a902717 100644 --- a/Sources/PaystackUI/Charge/ChargeViewModel.swift +++ b/Sources/PaystackUI/Charge/ChargeViewModel.swift @@ -98,7 +98,7 @@ extension ChargeViewModel { /// 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? = [ - "MPESA" + "MPESA", "ATL_KE", "MTN", "ATL", "VOD" ] } diff --git a/Sources/PaystackUI/Charge/MobileMoney/MobileMoneyFlowFactory.swift b/Sources/PaystackUI/Charge/MobileMoney/MobileMoneyFlowFactory.swift new file mode 100644 index 0000000..3171aea --- /dev/null +++ b/Sources/PaystackUI/Charge/MobileMoney/MobileMoneyFlowFactory.swift @@ -0,0 +1,15 @@ +import SwiftUI + +@available(iOS 14.0, *) +enum MobileMoneyFlowFactory { + + @ViewBuilder + static func view(for provider: MobileMoneyChannel, + chargeContainer: ChargeContainer, + transactionDetails: VerifyAccessCode) -> some View { + + MobileMoneyChargeView(chargeCardContainer: chargeContainer, + transactionDetails: transactionDetails, + provider: provider) + } +} diff --git a/Sources/PaystackUI/Charge/MobileMoney/Models/MobileMoneyChannel.swift b/Sources/PaystackUI/Charge/MobileMoney/Models/MobileMoneyChannel.swift index 78e4cde..3a577ad 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Models/MobileMoneyChannel.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Models/MobileMoneyChannel.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI import PaystackCore struct MobileMoneyChannel: Equatable { @@ -20,3 +21,32 @@ extension MobileMoneyChannel { .init(key: "MPESA", value: "M-PESA", isNew: true, phoneNumberRegex: "^\\+254(7([0-2]\\d|4\\d|5(7|8|9)|6(8|9)|9[0-9])|(11\\d))\\d{6}$") } } + +// MARK: - Provider-aware UI helpers + +extension MobileMoneyChannel { + + var expectedCountryCode: String { + switch key.uppercased() { + case "MPESA", "ATL_KE": + return "254" + case "MTN", "ATL", "VOD": // Ghana + return "233" + case "WAVE_CI", "ORANGE_CI", "MTN_CI": // Côte d'Ivoire + return "225" + default: + return "" + } + } + + var phoneInputAccessory: AnyView? { + switch key.uppercased() { + case "MPESA", "ATL_KE": + return AnyView(Image.kenyaFlagLogo) + case "MTN", "ATL", "VOD": + return AnyView(Image.ghanaFlagLogo) + default: + return nil + } + } +} diff --git a/Sources/PaystackUI/Charge/MobileMoney/Repository/ChargeMobileMoneyRepository.swift b/Sources/PaystackUI/Charge/MobileMoney/Repository/ChargeMobileMoneyRepository.swift index 5603ccf..31ff400 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Repository/ChargeMobileMoneyRepository.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Repository/ChargeMobileMoneyRepository.swift @@ -3,7 +3,7 @@ import PaystackCore protocol ChargeMobileMoneyRepository { func chargeMobileMoney(phone: String, transactionId: String, provider: String) async throws -> MobileMoneyTransaction - func listenForMPesa(for transactionId: Int) async throws -> ChargeCardTransaction + func listenForMobileMoneyResponse(for transactionId: Int) async throws -> ChargeCardTransaction func checkPendingCharge(with accessCode: String) async throws -> ChargeCardTransaction } @@ -21,7 +21,7 @@ struct ChargeMobileMoneyRepositoryImplementation: ChargeMobileMoneyRepository { return MobileMoneyTransaction.from(response) } - func listenForMPesa(for transactionId: Int) async throws -> ChargeCardTransaction { + func listenForMobileMoneyResponse(for transactionId: Int) async throws -> ChargeCardTransaction { let response = try await paystack.listenForMobileMoneyResponse(for: transactionId).async() return ChargeCardTransaction.from(response) } diff --git a/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaChrageViewModel.swift b/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MobileMoneyChargeViewModel.swift similarity index 77% rename from Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaChrageViewModel.swift rename to Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MobileMoneyChargeViewModel.swift index aaa535b..71d8375 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaChrageViewModel.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MobileMoneyChargeViewModel.swift @@ -1,14 +1,15 @@ import Foundation import PaystackCore -protocol MPesaContainer { +protocol MobileMoneyContainer { var transactionDetails: VerifyAccessCode { get } + var provider: MobileMoneyChannel { get } func processTransactionResponse(_ response: ChargeCardTransaction) async func displayTransactionError(_ error: ChargeError) - func restartMPesaPayment() + func restartMobileMoneyPayment() } -class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { +class MobileMoneyChargeViewModel: ObservableObject, @MainActor MobileMoneyContainer { var chargeCardContainer: ChargeContainer var repository: ChargeMobileMoneyRepository @@ -31,14 +32,20 @@ class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { } var isValid: Bool { - phoneNumber.count >= 10 + if !provider.phoneNumberRegex.isEmpty, + let regex = try? NSRegularExpression(pattern: provider.phoneNumberRegex) { + let formatted = phoneNumber.formatted(for: provider) + let range = NSRange(location: 0, length: formatted.utf16.count) + return regex.firstMatch(in: formatted, range: range) != nil + } + return phoneNumber.count >= 10 } @MainActor func submitPhoneNumber() async { do { let authenticationResult = try await repository.chargeMobileMoney( - phone: phoneNumber.formattedKenyanPhoneNumber, + phone: phoneNumber.formatted(for: provider), transactionId: "\(transactionDetails.transactionId ?? 0)", provider: provider.key) transactionState = .processTransaction(transaction: authenticationResult) @@ -47,7 +54,7 @@ class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { } } - func restartMPesaPayment() { + func restartMobileMoneyPayment() { transactionState = .countdown } @@ -65,7 +72,7 @@ class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { case .pending: break default: - Logger.error("Unexpected M-Pesa transaction status: %@", + Logger.error("Unexpected mobile money transaction status: %@", arguments: response.status.rawValue) transactionState = .fatalError( error: .generic(withCause: "Unexpected transaction status: \(response.status.rawValue)")) @@ -79,7 +86,7 @@ class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { } func cancelTransaction() { - restartMPesaPayment() + restartMobileMoneyPayment() } } diff --git a/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaProcessingViewModel.swift b/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MobileMoneyProcessingViewModel.swift similarity index 69% rename from Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaProcessingViewModel.swift rename to Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MobileMoneyProcessingViewModel.swift index f8613b8..22da104 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaProcessingViewModel.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MobileMoneyProcessingViewModel.swift @@ -1,15 +1,15 @@ import Foundation import PaystackCore -class MPesaProcessingViewModel: ObservableObject { +class MobileMoneyProcessingViewModel: ObservableObject { - var container: MPesaContainer + var container: MobileMoneyContainer var repository: ChargeMobileMoneyRepository let mobileMoneyTransaction: MobileMoneyTransaction @Published var counter = 0 - init(container: MPesaContainer, + init(container: MobileMoneyContainer, mobileMoneyTransaction: MobileMoneyTransaction, repository: ChargeMobileMoneyRepository = ChargeMobileMoneyRepositoryImplementation()) { self.container = container @@ -21,6 +21,13 @@ class MPesaProcessingViewModel: ObservableObject { container.transactionDetails } + var authorizationPromptText: String { + if !mobileMoneyTransaction.message.isEmpty { + return mobileMoneyTransaction.message + } + return "Please authorize the payment with \(container.provider.value) on your phone" + } + func checkTransactionStatus() { Task { await checkPendingCharge() @@ -28,13 +35,13 @@ class MPesaProcessingViewModel: ObservableObject { } @MainActor - func initializeMPesaAuthorization() async { + func initializeMobileMoneyAuthorization() async { do { - let authenticationResult = try await repository.listenForMPesa( + let authenticationResult = try await repository.listenForMobileMoneyResponse( for: Int(mobileMoneyTransaction.transaction) ?? 0) await container.processTransactionResponse(authenticationResult) } catch { - Logger.error("Listening for M-Pesa transaction failed with error: %@", + Logger.error("Listening for mobile money transaction failed with error: %@", arguments: error.localizedDescription) container.displayTransactionError(ChargeError(error: error)) } @@ -47,14 +54,14 @@ class MPesaProcessingViewModel: ObservableObject { with: transactionDetails.accessCode) await container.processTransactionResponse(authenticationResult) } catch { - Logger.error("Checking pending M-Pesa charge failed with error: %@", + Logger.error("Checking pending mobile money charge failed with error: %@", arguments: error.localizedDescription) container.displayTransactionError(ChargeError(error: error)) } } func cancelTransaction() { - container.restartMPesaPayment() + container.restartMobileMoneyPayment() } } diff --git a/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaChargeView.swift b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyChargeView.swift similarity index 83% rename from Sources/PaystackUI/Charge/MobileMoney/Views/MPesaChargeView.swift rename to Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyChargeView.swift index bb0ba7b..45e1bbe 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaChargeView.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyChargeView.swift @@ -1,10 +1,10 @@ import SwiftUI @available(iOS 14.0, *) -struct MPesaChargeView: View { +struct MobileMoneyChargeView: View { @StateObject - var viewModel: MPesaChrageViewModel + var viewModel: MobileMoneyChargeViewModel private let phoneNumberMaximumLength = 15 @State private var showPhoneNumberError = false @@ -12,7 +12,7 @@ struct MPesaChargeView: View { chargeCardContainer: ChargeContainer, transactionDetails: VerifyAccessCode, provider: MobileMoneyChannel) { - self._viewModel = StateObject(wrappedValue: MPesaChrageViewModel( + self._viewModel = StateObject(wrappedValue: MobileMoneyChargeViewModel( chargeCardContainer: chargeCardContainer, transactionDetails: transactionDetails, provider: provider)) @@ -25,15 +25,15 @@ struct MPesaChargeView: View { case .error(let chargeError): ErrorView(message: chargeError.message, buttonText: "Try again", - buttonAction: viewModel.restartMPesaPayment) + buttonAction: viewModel.restartMobileMoneyPayment) case .fatalError(let error): ErrorView(message: error.message, automaticallyDismissWith: .init( error: error, transactionReference: viewModel.transactionDetails.reference)) case .processTransaction(let transaction): - MPesaProcessingView(container: viewModel, - mobileMoneyTransaction: transaction) + MobileMoneyProcessingView(container: viewModel, + mobileMoneyTransaction: transaction) case .countdown: VStack(spacing: .triplePadding) { @@ -55,6 +55,7 @@ struct MPesaChargeView: View { @ViewBuilder var phoneNumber: some FormInputItemView { + TextFieldFormInputView(title: "Phone Number", placeholder: "070 000 0000", text: $viewModel.phoneNumber, @@ -62,7 +63,7 @@ struct MPesaChargeView: View { maxLength: phoneNumberMaximumLength, inErrorState: $showPhoneNumberError, defaultFocused: true, - accessoryView: Image.kenyaFlagLogo) + accessoryView: viewModel.provider.phoneInputAccessory) .minLength(10, errorMessage: "Invalid Phone Number") } } diff --git a/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaProcessingView.swift b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyProcessingView.swift similarity index 91% rename from Sources/PaystackUI/Charge/MobileMoney/Views/MPesaProcessingView.swift rename to Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyProcessingView.swift index 5d6e54e..efd726f 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaProcessingView.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyProcessingView.swift @@ -1,14 +1,14 @@ import SwiftUI @available(iOS 14.0, *) -struct MPesaProcessingView: View { +struct MobileMoneyProcessingView: View { @StateObject - var viewModel: MPesaProcessingViewModel + var viewModel: MobileMoneyProcessingViewModel - init(container: MPesaContainer, + init(container: MobileMoneyContainer, mobileMoneyTransaction: MobileMoneyTransaction) { - self._viewModel = StateObject(wrappedValue: MPesaProcessingViewModel( + self._viewModel = StateObject(wrappedValue: MobileMoneyProcessingViewModel( container: container, mobileMoneyTransaction: mobileMoneyTransaction)) } @@ -23,7 +23,7 @@ struct MPesaProcessingView: View { .foregroundColor(.stackBlue) .multilineTextAlignment(.center) - Text("Please enter your pin on your phone to complete this payment") + Text(viewModel.authorizationPromptText) .font(.body16M) .foregroundColor(.stackBlue) .multilineTextAlignment(.center) @@ -33,7 +33,7 @@ struct MPesaProcessingView: View { action: viewModel.checkTransactionStatus) } .padding(.doublePadding) - .task(viewModel.initializeMPesaAuthorization) + .task(viewModel.initializeMobileMoneyAuthorization) } } diff --git a/Sources/PaystackUI/Charge/Models/SupportedChannel.swift b/Sources/PaystackUI/Charge/Models/SupportedChannel.swift index 9cb863d..dd87e26 100644 --- a/Sources/PaystackUI/Charge/Models/SupportedChannel.swift +++ b/Sources/PaystackUI/Charge/Models/SupportedChannel.swift @@ -41,7 +41,9 @@ enum SupportedChannel: Equatable, Identifiable { /// future provider lights up via the allowlist. private static func image(forMobileMoneyKey key: String) -> Image { switch key.uppercased() { - case "MPESA": + case "MPESA", "ATL_KE": + return Image("kenyaSHLogo", bundle: .current) + case "MTN", "ATL", "VOD": return Image("kenyaSHLogo", bundle: .current) default: return Image(systemName: "creditcard") diff --git a/Sources/PaystackUI/Images/Images.swift b/Sources/PaystackUI/Images/Images.swift index 9e92a95..7cdf492 100644 --- a/Sources/PaystackUI/Images/Images.swift +++ b/Sources/PaystackUI/Images/Images.swift @@ -41,6 +41,11 @@ extension Image { Image("kenyaFlagLogo", bundle: .current) .frame(height: 16) } + + static var ghanaFlagLogo: some View { + Image("ghanaFlagLogo", bundle: .current) + .frame(height: 16) + } static var messageBubbleLogo: some View { Image("messageBubbleLogo", bundle: .current) diff --git a/Sources/PaystackUI/Images/Images.xcassets/ghanaFlagLogo.imageset/Contents.json b/Sources/PaystackUI/Images/Images.xcassets/ghanaFlagLogo.imageset/Contents.json new file mode 100644 index 0000000..42fbc55 --- /dev/null +++ b/Sources/PaystackUI/Images/Images.xcassets/ghanaFlagLogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Flags.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/PaystackUI/Images/Images.xcassets/ghanaFlagLogo.imageset/Flags.png b/Sources/PaystackUI/Images/Images.xcassets/ghanaFlagLogo.imageset/Flags.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d2bc2d462b78711760b3c3d8957032d92085e2 GIT binary patch literal 421 zcmeAS@N?(olHy`uVBq!ia0vp^5GZx^prwfgF}}M_)$E)e-c@Ne7+Lbh?3y^w370~qErUA%=FyEc^juC19d5Q zx;TbZFrJ;VH;c(pr1kt=ot!XU!AVLkJc_PC?KTdx_PES?!g`5ilAR{AU=EACqs~^* zgNNJ%vU5X3LINT`u-6w{pUD{LI3xBhtG&cCqdS)A<>!l5Rti_eM~JkhvOZL6xyAXg z{nf(&QL(&-ylsybTBxr$kSrtJ^MJ>lEmCRY9EY^Vr3sVQ-(YP^cKWkB`#V=ir1R?K zN^`Hf&ehIL>Q^~e_{{j@7lY*Nsu_+B<})|+c_rVSb>c^%{Jh6sUFUU}F#lWfXL3xu z-nMi9?0+TM-$+okkXRpWo*mB9mCDQbQO$PAKdXwmIU8bX%GNdAn^b;jf5sIy6SI|9 zF26PYFS^9T`PmzxP0Hsi_XTe3Z)EIGa9+AhV}XC%fz%nx5}WF#pXc=1;;Y`U0~m@7 Mp00i_>zopr0BPl+Gynhq literal 0 HcmV?d00001 diff --git a/Sources/PaystackUI/Utils/StringExtensions.swift b/Sources/PaystackUI/Utils/StringExtensions.swift index 5fb01f9..6a7d294 100644 --- a/Sources/PaystackUI/Utils/StringExtensions.swift +++ b/Sources/PaystackUI/Utils/StringExtensions.swift @@ -12,16 +12,20 @@ extension String { from: self) } - var formattedKenyanPhoneNumber: String { + func formatted(for provider: MobileMoneyChannel) -> String { let trimmed = self.removingAllWhitespaces - if trimmed.hasPrefix("+254") { + let countryCode = provider.expectedCountryCode + + guard !countryCode.isEmpty else { return trimmed } + + if trimmed.hasPrefix("+\(countryCode)") { return trimmed } - if trimmed.hasPrefix("254") { + if trimmed.hasPrefix(countryCode) { return "+" + trimmed } if trimmed.hasPrefix("0") { - return "+254" + trimmed.dropFirst() + return "+\(countryCode)" + trimmed.dropFirst() } return trimmed } diff --git a/Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift new file mode 100644 index 0000000..488d8c8 --- /dev/null +++ b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift @@ -0,0 +1,84 @@ +import XCTest +@testable import PaystackUI + +final class MobileMoneyChannelTests: XCTestCase { + + // MARK: - expectedCountryCode + + func testExpectedCountryCodeReturnsKenyaCodeForMPesa() { + XCTAssertEqual(MobileMoneyChannel(key: "MPESA", + value: "", + isNew: false, + phoneNumberRegex: "").expectedCountryCode, + "254") + } + + func testExpectedCountryCodeReturnsGhanaCodeForGhanaianProviders() { + for key in ["MTN", "ATL", "VOD"] { + XCTAssertEqual(MobileMoneyChannel(key: key, + value: "", + isNew: false, + phoneNumberRegex: "").expectedCountryCode, + "233", + "Expected \(key) to map to Ghana (233)") + } + } + + func testExpectedCountryCodeReturnsIvoryCoastCodeForOrangeAndMoov() { + for key in ["ORANGE", "MOOV"] { + XCTAssertEqual(MobileMoneyChannel(key: key, + value: "", + isNew: false, + phoneNumberRegex: "").expectedCountryCode, + "225", + "Expected \(key) to map to Côte d'Ivoire (225)") + } + } + + func testExpectedCountryCodeReturnsRwandaCodeForAirtel() { + XCTAssertEqual(MobileMoneyChannel(key: "AIRTEL", + value: "", + isNew: false, + phoneNumberRegex: "").expectedCountryCode, + "250") + } + + func testExpectedCountryCodeReturnsEmptyForUnknownProvider() { + XCTAssertEqual(MobileMoneyChannel(key: "SOMETHING_NEW", + value: "", + isNew: false, + phoneNumberRegex: "").expectedCountryCode, + "") + } + + func testExpectedCountryCodeIsCaseInsensitive() { + XCTAssertEqual(MobileMoneyChannel(key: "mpesa", + value: "", + isNew: false, + phoneNumberRegex: "").expectedCountryCode, + "254") + XCTAssertEqual(MobileMoneyChannel(key: "Mtn", + value: "", + isNew: false, + phoneNumberRegex: "").expectedCountryCode, + "233") + } + + // MARK: - phoneInputAccessory + + func testPhoneInputAccessoryReturnsViewForMPesa() { + let mpesa = MobileMoneyChannel(key: "MPESA", + value: "M-PESA", + isNew: false, + phoneNumberRegex: "") + XCTAssertNotNil(mpesa.phoneInputAccessory) + } + + func testPhoneInputAccessoryReturnsNilForProviderWithoutShippedAsset() { + let unknown = MobileMoneyChannel(key: "SOMETHING_NEW", + value: "", + isNew: false, + phoneNumberRegex: "") + XCTAssertNil(unknown.phoneInputAccessory) + } +} diff --git a/Tests/PaystackSDKTests/UI/Charge/MPesaCharge/MPesaChrageViewModelTests.swift b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyCharge/MobileMoneyChargeViewModelTests.swift similarity index 73% rename from Tests/PaystackSDKTests/UI/Charge/MPesaCharge/MPesaChrageViewModelTests.swift rename to Tests/PaystackSDKTests/UI/Charge/MobileMoneyCharge/MobileMoneyChargeViewModelTests.swift index 4c9895f..7616153 100644 --- a/Tests/PaystackSDKTests/UI/Charge/MPesaCharge/MPesaChrageViewModelTests.swift +++ b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyCharge/MobileMoneyChargeViewModelTests.swift @@ -2,9 +2,9 @@ import XCTest import PaystackCore @testable import PaystackUI -final class MPesaChrageViewModelTests: XCTestCase { +final class MobileMoneyChargeViewModelTests: XCTestCase { - var serviceUnderTest: MPesaChrageViewModel! + var serviceUnderTest: MobileMoneyChargeViewModel! var mockChargeContainer: MockChargeContainer! var mockRepository: MockChargeMobileMoneyRepository! @@ -12,24 +12,54 @@ final class MPesaChrageViewModelTests: XCTestCase { try super.setUpWithError() mockChargeContainer = MockChargeContainer() mockRepository = MockChargeMobileMoneyRepository() - serviceUnderTest = MPesaChrageViewModel(chargeCardContainer: mockChargeContainer, - transactionDetails: .example, - provider: .example, - repository: mockRepository) + serviceUnderTest = MobileMoneyChargeViewModel(chargeCardContainer: mockChargeContainer, + transactionDetails: .example, + provider: .example, + repository: mockRepository) } // MARK: - Phone number validation - func testIsValidReturnsFalseWhenPhoneNumberIsLessThanTenDigits() { - serviceUnderTest.phoneNumber = "012345678" - XCTAssertFalse(serviceUnderTest.isValid) + func testIsValidReturnsTrueWhenPhoneMatchesProviderRegex() { + // .example is MPESA: the regex requires +254 followed by 7xx or 11x. + // 0703... formats to +254703... which satisfies the 7([0-2]\d|...) branch. + serviceUnderTest.phoneNumber = "0703362111" + XCTAssertTrue(serviceUnderTest.isValid) } - func testIsValidReturnsTrueWhenPhoneNumberIsAtLeastTenDigits() { + func testIsValidReturnsFalseWhenFormattedPhoneFailsProviderRegex() { + // Same provider (MPESA) but the formatted input (+254123456789) doesn't + // match the regex (first digit after +254 must be 7 or 1-then-1). serviceUnderTest.phoneNumber = "0123456789" + XCTAssertFalse(serviceUnderTest.isValid) + } + + func testIsValidFallsBackToMinLengthWhenProviderHasNoRegex() { + let providerWithoutRegex = MobileMoneyChannel(key: "UNKNOWN", + value: "Unknown", + isNew: false, + phoneNumberRegex: "") + serviceUnderTest = MobileMoneyChargeViewModel(chargeCardContainer: mockChargeContainer, + transactionDetails: .example, + provider: providerWithoutRegex, + repository: mockRepository) + serviceUnderTest.phoneNumber = "0123456789" // 10 digits — passes minLength XCTAssertTrue(serviceUnderTest.isValid) } + func testIsValidFallsBackToMinLengthAndFailsForShortInput() { + let providerWithoutRegex = MobileMoneyChannel(key: "UNKNOWN", + value: "Unknown", + isNew: false, + phoneNumberRegex: "") + serviceUnderTest = MobileMoneyChargeViewModel(chargeCardContainer: mockChargeContainer, + transactionDetails: .example, + provider: providerWithoutRegex, + repository: mockRepository) + serviceUnderTest.phoneNumber = "012345678" // 9 digits — fails minLength + XCTAssertFalse(serviceUnderTest.isValid) + } + // MARK: - Initial state func testInitialStateIsCountdown() { @@ -50,10 +80,10 @@ final class MPesaChrageViewModelTests: XCTestCase { reference: "test_reference", transactionId: expectedTransactionId, channelOptions: .example) - serviceUnderTest = MPesaChrageViewModel(chargeCardContainer: mockChargeContainer, - transactionDetails: transactionDetails, - provider: .example, - repository: mockRepository) + serviceUnderTest = MobileMoneyChargeViewModel(chargeCardContainer: mockChargeContainer, + transactionDetails: transactionDetails, + provider: .example, + repository: mockRepository) mockRepository.expectedMobileMoneyTransaction = .mPesaExample serviceUnderTest.phoneNumber = "0703362111" @@ -96,10 +126,10 @@ final class MPesaChrageViewModelTests: XCTestCase { value: "MTN", isNew: false, phoneNumberRegex: "") - serviceUnderTest = MPesaChrageViewModel(chargeCardContainer: mockChargeContainer, - transactionDetails: .example, - provider: mtnProvider, - repository: mockRepository) + serviceUnderTest = MobileMoneyChargeViewModel(chargeCardContainer: mockChargeContainer, + transactionDetails: .example, + provider: mtnProvider, + repository: mockRepository) mockRepository.expectedMobileMoneyTransaction = .mPesaExample serviceUnderTest.phoneNumber = "0703362111" @@ -196,9 +226,9 @@ final class MPesaChrageViewModelTests: XCTestCase { XCTAssertEqual(serviceUnderTest.transactionState, .error(error)) } - func testRestartMPesaPaymentResetsStateToCountdown() { + func testRestartMobileMoneyPaymentResetsStateToCountdown() { serviceUnderTest.transactionState = .error(.generic) - serviceUnderTest.restartMPesaPayment() + serviceUnderTest.restartMobileMoneyPayment() XCTAssertEqual(serviceUnderTest.transactionState, .countdown) } diff --git a/Tests/PaystackSDKTests/UI/Charge/MPesaProcessing/MPesaProcessingViewModelTests.swift b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyProcessing/MobileMoneyProcessingViewModelTests.swift similarity index 52% rename from Tests/PaystackSDKTests/UI/Charge/MPesaProcessing/MPesaProcessingViewModelTests.swift rename to Tests/PaystackSDKTests/UI/Charge/MobileMoneyProcessing/MobileMoneyProcessingViewModelTests.swift index c28a26d..a232ed3 100644 --- a/Tests/PaystackSDKTests/UI/Charge/MPesaProcessing/MPesaProcessingViewModelTests.swift +++ b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyProcessing/MobileMoneyProcessingViewModelTests.swift @@ -2,62 +2,62 @@ import XCTest import PaystackCore @testable import PaystackUI -final class MPesaProcessingViewModelTests: XCTestCase { +final class MobileMoneyProcessingViewModelTests: XCTestCase { - var serviceUnderTest: MPesaProcessingViewModel! - var mockContainer: MockMPesaContainer! + var serviceUnderTest: MobileMoneyProcessingViewModel! + var mockContainer: MockMobileMoneyContainer! var mockRepository: MockChargeMobileMoneyRepository! var mobileMoneyTransaction: MobileMoneyTransaction! override func setUpWithError() throws { try super.setUpWithError() - mockContainer = MockMPesaContainer() + mockContainer = MockMobileMoneyContainer() mockRepository = MockChargeMobileMoneyRepository() mobileMoneyTransaction = .mPesaExample - serviceUnderTest = MPesaProcessingViewModel(container: mockContainer, - mobileMoneyTransaction: mobileMoneyTransaction, - repository: mockRepository) + serviceUnderTest = MobileMoneyProcessingViewModel(container: mockContainer, + mobileMoneyTransaction: mobileMoneyTransaction, + repository: mockRepository) } - // MARK: - initializeMPesaAuthorization + // MARK: - initializeMobileMoneyAuthorization - func testInitializeMPesaAuthorizationForwardsTransactionIdToRepository() async { + func testInitializeMobileMoneyAuthorizationForwardsTransactionIdToRepository() async { mockRepository.expectedChargeCardTransaction = .example - await serviceUnderTest.initializeMPesaAuthorization() - XCTAssertEqual(mockRepository.listenForMPesaTransactionId, 1504248187) + await serviceUnderTest.initializeMobileMoneyAuthorization() + XCTAssertEqual(mockRepository.listenForMobileMoneyResponseTransactionId, 1504248187) } - func testInitializeMPesaAuthorizationWithNonNumericTransactionDefaultsToZero() async { + func testInitializeMobileMoneyAuthorizationWithNonNumericTransactionDefaultsToZero() async { mobileMoneyTransaction = MobileMoneyTransaction(transaction: "not-a-number", phone: "0703362111", provider: "MPESA", channelName: "MOBILE_MONEY_x", timer: 60, message: "") - serviceUnderTest = MPesaProcessingViewModel(container: mockContainer, - mobileMoneyTransaction: mobileMoneyTransaction, - repository: mockRepository) + serviceUnderTest = MobileMoneyProcessingViewModel(container: mockContainer, + mobileMoneyTransaction: mobileMoneyTransaction, + repository: mockRepository) mockRepository.expectedChargeCardTransaction = .example - await serviceUnderTest.initializeMPesaAuthorization() + await serviceUnderTest.initializeMobileMoneyAuthorization() - XCTAssertEqual(mockRepository.listenForMPesaTransactionId, 0) + XCTAssertEqual(mockRepository.listenForMobileMoneyResponseTransactionId, 0) } - func testInitializeMPesaAuthorizationOnSuccessForwardsResponseToContainer() async { + func testInitializeMobileMoneyAuthorizationOnSuccessForwardsResponseToContainer() async { let expectedResponse = ChargeCardTransaction(status: .success) mockRepository.expectedChargeCardTransaction = expectedResponse - await serviceUnderTest.initializeMPesaAuthorization() + await serviceUnderTest.initializeMobileMoneyAuthorization() XCTAssertEqual(mockContainer.transactionResponse, expectedResponse) } - func testInitializeMPesaAuthorizationOnErrorForwardsErrorToContainer() async { + func testInitializeMobileMoneyAuthorizationOnErrorForwardsErrorToContainer() async { let expectedErrorMessage = "Subscription failed" mockRepository.expectedErrorResponse = PaystackError.response(code: 500, message: expectedErrorMessage) - await serviceUnderTest.initializeMPesaAuthorization() + await serviceUnderTest.initializeMobileMoneyAuthorization() XCTAssertEqual(mockContainer.transactionError, ChargeError(message: expectedErrorMessage)) } @@ -96,7 +96,7 @@ final class MPesaProcessingViewModelTests: XCTestCase { func testCancelTransactionAsksContainerToRestart() { serviceUnderTest.cancelTransaction() - XCTAssertTrue(mockContainer.mPesaPaymentRestarted) + XCTAssertTrue(mockContainer.mobileMoneyPaymentRestarted) } // MARK: - transactionDetails @@ -104,6 +104,34 @@ final class MPesaProcessingViewModelTests: XCTestCase { func testTransactionDetailsComesFromContainer() { XCTAssertEqual(serviceUnderTest.transactionDetails, mockContainer.transactionDetails) } + + // MARK: - authorizationPromptText + + func testAuthorizationPromptTextUsesApiMessageWhenPresent() { + // mPesaExample carries `message: "Authorize on your device"` — the + // API copy should win over the SDK fallback even when the provider + // is set. + XCTAssertEqual(serviceUnderTest.authorizationPromptText, + "Authorize on your device") + } + + func testAuthorizationPromptTextFallsBackToProviderCopyWhenApiMessageEmpty() { + let transactionWithoutMessage = MobileMoneyTransaction(transaction: "1234", + phone: "0703362111", + provider: "MPESA", + channelName: "MOBILE_MONEY_1234", + timer: 60, + message: "") + mockContainer.provider = MobileMoneyChannel(key: "MTN", + value: "MTN", + isNew: false, + phoneNumberRegex: "") + serviceUnderTest = MobileMoneyProcessingViewModel(container: mockContainer, + mobileMoneyTransaction: transactionWithoutMessage, + repository: mockRepository) + XCTAssertEqual(serviceUnderTest.authorizationPromptText, + "Please authorize the payment with MTN on your phone") + } } private extension MobileMoneyTransaction { diff --git a/Tests/PaystackSDKTests/UI/Charge/MobileMoneyRepository/ChargeMobileMoneyRepositoryImplementationTests.swift b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyRepository/ChargeMobileMoneyRepositoryImplementationTests.swift index a8bcae7..8a0ba5b 100644 --- a/Tests/PaystackSDKTests/UI/Charge/MobileMoneyRepository/ChargeMobileMoneyRepositoryImplementationTests.swift +++ b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyRepository/ChargeMobileMoneyRepositoryImplementationTests.swift @@ -30,7 +30,7 @@ final class ChargeMobileMoneyRepositoryImplementationTests: PSTestCase { XCTAssertEqual(result, .jsonExample) } - func testListenForMPesaSubscribesToMobileMoneyChannelAndMapsSuccessToSuccess() async throws { + func testListenForMobileMoneyResponseSubscribesToMobileMoneyChannelAndMapsSuccessToSuccess() async throws { let transactionId = 1234 let mockSubscription = PusherSubscription(channelName: "MOBILE_MONEY_\(transactionId)", eventName: "response") @@ -41,11 +41,11 @@ final class ChargeMobileMoneyRepositoryImplementationTests: PSTestCase { .expectSubscription(mockSubscription) .andReturnString(responseString) - let result = try await serviceUnderTest.listenForMPesa(for: transactionId) + let result = try await serviceUnderTest.listenForMobileMoneyResponse(for: transactionId) XCTAssertEqual(result, .init(status: .success)) } - func testListenForMPesaMapsFailedSubscriptionResponseToFailedStatus() async throws { + func testListenForMobileMoneyResponseMapsFailedSubscriptionResponseToFailedStatus() async throws { let transactionId = 4321 let mockSubscription = PusherSubscription(channelName: "MOBILE_MONEY_\(transactionId)", eventName: "response") @@ -56,7 +56,7 @@ final class ChargeMobileMoneyRepositoryImplementationTests: PSTestCase { .expectSubscription(mockSubscription) .andReturnString(responseString) - let result = try await serviceUnderTest.listenForMPesa(for: transactionId) + let result = try await serviceUnderTest.listenForMobileMoneyResponse(for: transactionId) XCTAssertEqual(result, .init(status: .failed)) } diff --git a/Tests/PaystackSDKTests/UI/Charge/Mocks/MockChargeMobileMoneyRepository.swift b/Tests/PaystackSDKTests/UI/Charge/Mocks/MockChargeMobileMoneyRepository.swift index f7e9054..1737add 100644 --- a/Tests/PaystackSDKTests/UI/Charge/Mocks/MockChargeMobileMoneyRepository.swift +++ b/Tests/PaystackSDKTests/UI/Charge/Mocks/MockChargeMobileMoneyRepository.swift @@ -9,7 +9,7 @@ class MockChargeMobileMoneyRepository: ChargeMobileMoneyRepository { var chargeMobileMoneySubmitted: (phone: String, transactionId: String, provider: String) = ("", "", "") - var listenForMPesaTransactionId: Int? + var listenForMobileMoneyResponseTransactionId: Int? var pendingChargeAccessCode: String? func chargeMobileMoney(phone: String, transactionId: String, @@ -21,8 +21,8 @@ class MockChargeMobileMoneyRepository: ChargeMobileMoneyRepository { return response } - func listenForMPesa(for transactionId: Int) async throws -> ChargeCardTransaction { - listenForMPesaTransactionId = transactionId + func listenForMobileMoneyResponse(for transactionId: Int) async throws -> ChargeCardTransaction { + listenForMobileMoneyResponseTransactionId = transactionId guard let response = expectedChargeCardTransaction else { throw expectedErrorResponse ?? MockError.stubNotProvided } diff --git a/Tests/PaystackSDKTests/UI/Charge/Mocks/MockMPesaContainer.swift b/Tests/PaystackSDKTests/UI/Charge/Mocks/MockMobileMoneyContainer.swift similarity index 72% rename from Tests/PaystackSDKTests/UI/Charge/Mocks/MockMPesaContainer.swift rename to Tests/PaystackSDKTests/UI/Charge/Mocks/MockMobileMoneyContainer.swift index 1abdb58..7c0a273 100644 --- a/Tests/PaystackSDKTests/UI/Charge/Mocks/MockMPesaContainer.swift +++ b/Tests/PaystackSDKTests/UI/Charge/Mocks/MockMobileMoneyContainer.swift @@ -1,11 +1,12 @@ import Foundation @testable import PaystackUI -class MockMPesaContainer: MPesaContainer { +class MockMobileMoneyContainer: MobileMoneyContainer { var transactionDetails: VerifyAccessCode = .example + var provider: MobileMoneyChannel = .example - var mPesaPaymentRestarted = false + var mobileMoneyPaymentRestarted = false var transactionResponse: ChargeCardTransaction? var transactionError: ChargeError? @@ -22,7 +23,7 @@ class MockMPesaContainer: MPesaContainer { onDisplayTransactionError?() } - func restartMPesaPayment() { - mPesaPaymentRestarted = true + func restartMobileMoneyPayment() { + mobileMoneyPaymentRestarted = true } } diff --git a/Tests/PaystackSDKTests/UI/Utils/StringExtensionsTests.swift b/Tests/PaystackSDKTests/UI/Utils/StringExtensionsTests.swift index 607392b..bb6ec9f 100644 --- a/Tests/PaystackSDKTests/UI/Utils/StringExtensionsTests.swift +++ b/Tests/PaystackSDKTests/UI/Utils/StringExtensionsTests.swift @@ -3,30 +3,59 @@ import XCTest final class StringExtensionsTests: XCTestCase { - // MARK: - formattedKenyanPhoneNumber + // MARK: - formatted(for:) - func testFormattedKenyanPhoneNumberWithLeadingZeroReplacesZeroWithCountryCode() { + func testFormattedForKenyanProviderWithLeadingZeroReplacesZeroWithCountryCode() { let phone = "0703362111" - XCTAssertEqual(phone.formattedKenyanPhoneNumber, "+254703362111") + XCTAssertEqual(phone.formatted(for: .mpesa), "+254703362111") } - func testFormattedKenyanPhoneNumberWithLeading254PrependsPlus() { + func testFormattedForKenyanProviderWithLeading254PrependsPlus() { let phone = "254703362111" - XCTAssertEqual(phone.formattedKenyanPhoneNumber, "+254703362111") + XCTAssertEqual(phone.formatted(for: .mpesa), "+254703362111") } - func testFormattedKenyanPhoneNumberWithLeadingPlus254ReturnsSameNumber() { + func testFormattedForKenyanProviderWithLeadingPlus254ReturnsSameNumber() { let phone = "+254703362111" - XCTAssertEqual(phone.formattedKenyanPhoneNumber, "+254703362111") + XCTAssertEqual(phone.formatted(for: .mpesa), "+254703362111") } - func testFormattedKenyanPhoneNumberStripsWhitespaceBeforeFormatting() { + func testFormattedForStripsWhitespaceBeforeFormatting() { let phone = " 0703 362 111 " - XCTAssertEqual(phone.formattedKenyanPhoneNumber, "+254703362111") + XCTAssertEqual(phone.formatted(for: .mpesa), "+254703362111") } - func testFormattedKenyanPhoneNumberWithUnrecognisedPrefixReturnsInputWithoutWhitespace() { + func testFormattedForWithUnrecognisedPrefixReturnsInputWithoutWhitespace() { let phone = "7033 62111" - XCTAssertEqual(phone.formattedKenyanPhoneNumber, "703362111") + XCTAssertEqual(phone.formatted(for: .mpesa), "703362111") } + + // MARK: - Cross-provider behaviour + + func testFormattedForGhanaianProviderUsesGhanaCountryCode() { + let phone = "0241234567" + XCTAssertEqual(phone.formatted(for: .mtn), "+233241234567") + } + + func testFormattedForReturnsTrimmedInputWhenProviderHasNoCountryCode() { + let unknownProvider = MobileMoneyChannel(key: "UNKNOWN", + value: "Unknown", + isNew: false, + phoneNumberRegex: "") + let phone = "0703362111" + XCTAssertEqual(phone.formatted(for: unknownProvider), "0703362111") + } +} + +// MARK: - Test fixtures + +private extension MobileMoneyChannel { + static let mpesa = MobileMoneyChannel(key: "MPESA", + value: "M-PESA", + isNew: true, + phoneNumberRegex: "") + static let mtn = MobileMoneyChannel(key: "MTN", + value: "MTN", + isNew: false, + phoneNumberRegex: "") } From 03eb4d7c7ad41338b3a7a4a0d6d5ab74caaadd00 Mon Sep 17 00:00:00 2001 From: Peter-John Welcome Date: Wed, 20 May 2026 16:38:58 +0200 Subject: [PATCH 2/2] Updating tests to reflect the correct keys --- .../MobileMoneyChannel/MobileMoneyChannelTests.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift index 488d8c8..34f8a07 100644 --- a/Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift +++ b/Tests/PaystackSDKTests/UI/Charge/MobileMoneyChannel/MobileMoneyChannelTests.swift @@ -25,7 +25,7 @@ final class MobileMoneyChannelTests: XCTestCase { } func testExpectedCountryCodeReturnsIvoryCoastCodeForOrangeAndMoov() { - for key in ["ORANGE", "MOOV"] { + for key in ["WAVE_CI", "ORANGE_CI", "MTN_CI"] { XCTAssertEqual(MobileMoneyChannel(key: key, value: "", isNew: false, @@ -35,13 +35,6 @@ final class MobileMoneyChannelTests: XCTestCase { } } - func testExpectedCountryCodeReturnsRwandaCodeForAirtel() { - XCTAssertEqual(MobileMoneyChannel(key: "AIRTEL", - value: "", - isNew: false, - phoneNumberRegex: "").expectedCountryCode, - "250") - } func testExpectedCountryCodeReturnsEmptyForUnknownProvider() { XCTAssertEqual(MobileMoneyChannel(key: "SOMETHING_NEW",