diff --git a/Sources/PaystackUI/Charge/ChargeView.swift b/Sources/PaystackUI/Charge/ChargeView.swift index 85509e5..abb5f4d 100644 --- a/Sources/PaystackUI/Charge/ChargeView.swift +++ b/Sources/PaystackUI/Charge/ChargeView.swift @@ -48,7 +48,7 @@ struct ChargeView: View { .aspectRatio(contentMode: .fit) .frame(width: 140) } - .task(viewModel.verifyAccessCodeAndProceedWithCard) + .task(viewModel.verifyAccessCodeAndProceed) .modalCancelButton(showConfirmation: viewModel.displayCloseButtonConfirmation, onCancelled: chargeCancelled) } @@ -59,8 +59,10 @@ struct ChargeView: View { case .card(let transactionInformation): ChargeCardView(transactionDetails: transactionInformation, chargeContainer: viewModel) - case .mobileMoney(transactionInformation: let transactionInformation): - MPesaChargeView(chargeCardContainer: viewModel, transactionDetails: transactionInformation) + case .mobileMoney(let transactionInformation, let provider): + MPesaChargeView(chargeCardContainer: viewModel, + transactionDetails: transactionInformation, + provider: provider) } } diff --git a/Sources/PaystackUI/Charge/ChargeViewModel.swift b/Sources/PaystackUI/Charge/ChargeViewModel.swift index 98e2277..9097020 100644 --- a/Sources/PaystackUI/Charge/ChargeViewModel.swift +++ b/Sources/PaystackUI/Charge/ChargeViewModel.swift @@ -19,37 +19,22 @@ class ChargeViewModel: ObservableObject { } @MainActor - func verifyAccessCodeAndProceedWithCard() async { - var supportedChannels: [SupportedChannels] = [] + func verifyAccessCodeAndProceed() async { do { transactionState = .loading() - let accessCodeResponse = try await repository.verifyAccessCode(accessCode) - guard accessCodeResponse.paymentChannels.contains(where: { $0 == .card || $0 == .mobileMoney }) else { - let message = "Card/MPesa payments are not supported. " + + 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 = "There are currently no payment channels on " + - "your integration that are supported by the SDK" + let cause = "No payment channels on this integration " + + "are supported by the SDK" throw ChargeError(displayMessage: message, causeMessage: cause) } - let mobileMoneyChannel = accessCodeResponse.channelOptions?.mobileMoney?.contains(where: { $0.key == SupportedChannels.MPESA.rawValue }) ?? false - - accessCodeResponse.paymentChannels.forEach { - if $0 == .card { - supportedChannels.append(.CARD) - } - if $0 == .mobileMoney && accessCodeResponse.channelOptions?.mobileMoney?.contains(where: { $0.key == SupportedChannels.MPESA.rawValue }) ?? false { - supportedChannels.append(.MPESA) - } - } - - if mobileMoneyChannel { - transactionState = .channelSelection( - transactionInformation: accessCodeResponse, supportedChannels: supportedChannels) - } else { - self.transactionDetails = accessCodeResponse - transactionState = .payment(type: .card(transactionInformation: accessCodeResponse)) - } + transactionDetails = response + transactionState = nextState(for: supported, response: response) } catch { let error = ChargeError(error: error) Logger.error("Verify access code failed with error: %@", @@ -59,24 +44,62 @@ class ChargeViewModel: ObservableObject { } } -} + 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 + } -enum SupportedChannels: String, CaseIterable { - case CARD = "CARD" - case MPESA = "MPESA" - case unsupportedChannel - - var image: Image { - switch self { - case .CARD: - return Image("cardLogo", bundle: .current) - case .MPESA: - return Image("kenyaSHLogo", bundle: .current) - case .unsupportedChannel: - return Image(systemName: "exclamationmark.triangle.fill") + 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? = [ + "MPESA" + ] } // MARK: - Charge Container diff --git a/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaChrageViewModel.swift b/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaChrageViewModel.swift index 3299a15..aaa535b 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaChrageViewModel.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Viewmodels/MPesaChrageViewModel.swift @@ -13,6 +13,7 @@ class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { var chargeCardContainer: ChargeContainer var repository: ChargeMobileMoneyRepository var transactionDetails: VerifyAccessCode + let provider: MobileMoneyChannel @Published var phoneNumber: String = "" @@ -21,10 +22,12 @@ class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { init(chargeCardContainer: ChargeContainer, transactionDetails: VerifyAccessCode, + provider: MobileMoneyChannel, repository: ChargeMobileMoneyRepository = ChargeMobileMoneyRepositoryImplementation()) { self.chargeCardContainer = chargeCardContainer self.repository = repository self.transactionDetails = transactionDetails + self.provider = provider } var isValid: Bool { @@ -37,7 +40,7 @@ class MPesaChrageViewModel: ObservableObject, @MainActor MPesaContainer { let authenticationResult = try await repository.chargeMobileMoney( phone: phoneNumber.formattedKenyanPhoneNumber, transactionId: "\(transactionDetails.transactionId ?? 0)", - provider: transactionDetails.channelOptions?.mobileMoney?.first?.key ?? "") + provider: provider.key) transactionState = .processTransaction(transaction: authenticationResult) } catch { displayTransactionError(ChargeError(error: error)) diff --git a/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift b/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift index c5731bb..76f48d2 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift @@ -14,15 +14,14 @@ struct ChannelSelectionView: View { var visibilityContainer: ViewVisibilityContainer @StateObject var viewModel: ChannelSelectionViewModel + let supportedChannels: [SupportedChannel] let columns = [GridItem(.flexible()), GridItem(.flexible())] - var items: [PaymentChannel] = [] + init(state: Binding, - supportedChannels: [SupportedChannels], + supportedChannels: [SupportedChannel], information: VerifyAccessCode) { self._viewModel = StateObject(wrappedValue: ChannelSelectionViewModel(state: state, information: information)) - items = supportedChannels.map { - PaymentChannel(channel: $0) - } + self.supportedChannels = supportedChannels } var body: some View { @@ -35,13 +34,13 @@ struct ChannelSelectionView: View { .multilineTextAlignment(.center) GeometryReader { geo in LazyVGrid(columns: columns) { - ForEach(items) { value in - ChannelView(channelTitle: value.title, image: value.image) + ForEach(supportedChannels) { channel in + ChannelView(channelTitle: channel.displayTitle, image: channel.image) .padding(.singlePadding) .onTapGesture { - viewModel.chooseChannel(channel: value.channel) + viewModel.choose(channel) } - .frame(width: (geo.size.width / CGFloat(items.count)).rounded()) + .frame(width: (geo.size.width / CGFloat(supportedChannels.count)).rounded()) } } } @@ -59,31 +58,14 @@ class ChannelSelectionViewModel: ObservableObject { self.information = information } - func chooseChannel(channel: SupportedChannels) { - let message = "Card/MPesa payments are not supported. " + - "Please reach out to your merchant for further information" - let cause = "There are currently no payment channels on " + - "your integration that are supported by the SDK" + func choose(_ channel: SupportedChannel) { switch channel { - case .CARD: + case .card: state = .payment(type: .card(transactionInformation: self.information)) - case .MPESA: - state = .payment(type: .mobileMoney(transactionInformation: self.information)) - case .unsupportedChannel: - state = .error(ChargeError(displayMessage: message, causeMessage: cause)) + case .mobileMoney(let provider): + state = .payment(type: .mobileMoney(transactionInformation: self.information, + provider: provider)) } - - } -} - -struct PaymentChannel: Identifiable { - var id: String = UUID().uuidString - let channel: SupportedChannels - var title: String { - channel.rawValue - } - var image: Image { - channel.image } } diff --git a/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaChargeView.swift b/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaChargeView.swift index 04239cc..bb0ba7b 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaChargeView.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Views/MPesaChargeView.swift @@ -10,9 +10,12 @@ struct MPesaChargeView: View { init( chargeCardContainer: ChargeContainer, - transactionDetails: VerifyAccessCode) { + transactionDetails: VerifyAccessCode, + provider: MobileMoneyChannel) { self._viewModel = StateObject(wrappedValue: MPesaChrageViewModel( - chargeCardContainer: chargeCardContainer, transactionDetails: transactionDetails)) + chargeCardContainer: chargeCardContainer, + transactionDetails: transactionDetails, + provider: provider)) } var body: some View { diff --git a/Sources/PaystackUI/Charge/Models/ChargePaymentType.swift b/Sources/PaystackUI/Charge/Models/ChargePaymentType.swift index 61b82a8..53de317 100644 --- a/Sources/PaystackUI/Charge/Models/ChargePaymentType.swift +++ b/Sources/PaystackUI/Charge/Models/ChargePaymentType.swift @@ -3,5 +3,6 @@ import Foundation // TODO: Add an extension to map from payment channels once those are defined enum ChargePaymentType: Equatable { case card(transactionInformation: VerifyAccessCode) - case mobileMoney(transactionInformation: VerifyAccessCode) + case mobileMoney(transactionInformation: VerifyAccessCode, + provider: MobileMoneyChannel) } diff --git a/Sources/PaystackUI/Charge/Models/ChargeState.swift b/Sources/PaystackUI/Charge/Models/ChargeState.swift index 62e4130..1c274e5 100644 --- a/Sources/PaystackUI/Charge/Models/ChargeState.swift +++ b/Sources/PaystackUI/Charge/Models/ChargeState.swift @@ -4,7 +4,7 @@ import PaystackCore enum ChargeState { case loading(message: String? = nil) case payment(type: ChargePaymentType) - case channelSelection (transactionInformation: VerifyAccessCode, supportedChannels: [SupportedChannels]) + case channelSelection (transactionInformation: VerifyAccessCode, supportedChannels: [SupportedChannel]) case error(ChargeError) case success(amount: AmountCurrency, merchant: String, details: ChargeCompletionDetails) diff --git a/Sources/PaystackUI/Charge/Models/SupportedChannel.swift b/Sources/PaystackUI/Charge/Models/SupportedChannel.swift new file mode 100644 index 0000000..9cb863d --- /dev/null +++ b/Sources/PaystackUI/Charge/Models/SupportedChannel.swift @@ -0,0 +1,50 @@ +import SwiftUI +import PaystackCore + +/// A resolved payment channel that the SDK is willing to route to for the +/// current transaction. One entry per card option, plus one entry per +/// supported mobile money provider returned by `verifyAccessCode`. +enum SupportedChannel: Equatable, Identifiable { + case card + case mobileMoney(MobileMoneyChannel) + + var id: String { + switch self { + case .card: + return "card" + case .mobileMoney(let channel): + return "mobile_money.\(channel.key)" + } + } + + var displayTitle: String { + switch self { + case .card: + return "Card" + case .mobileMoney(let channel): + return channel.value + } + } + + var image: Image { + switch self { + case .card: + return Image("cardLogo", bundle: .current) + case .mobileMoney(let channel): + return Self.image(forMobileMoneyKey: channel.key) + } + } + + /// Maps known Paystack mobile money provider keys to a bundled logo. + /// Falls back to a generic SF Symbol when the SDK has no logo for the + /// provider yet — keeps the channel-selection screen renderable when a + /// future provider lights up via the allowlist. + private static func image(forMobileMoneyKey key: String) -> Image { + switch key.uppercased() { + case "MPESA": + return Image("kenyaSHLogo", bundle: .current) + default: + return Image(systemName: "creditcard") + } + } +} diff --git a/Tests/PaystackSDKTests/UI/Charge/ChargeViewModelTests.swift b/Tests/PaystackSDKTests/UI/Charge/ChargeViewModelTests.swift index 779c1b7..ebb6b24 100644 --- a/Tests/PaystackSDKTests/UI/Charge/ChargeViewModelTests.swift +++ b/Tests/PaystackSDKTests/UI/Charge/ChargeViewModelTests.swift @@ -1,4 +1,5 @@ import XCTest +import PaystackCore @testable import PaystackUI final class ChargeViewModelTests: PSTestCase { @@ -6,12 +7,23 @@ final class ChargeViewModelTests: PSTestCase { var serviceUnderTest: ChargeViewModel! var mockRepo: MockChargeRepository! + /// Snapshot the production allowlist on entry so individual tests can + /// mutate `supportedMobileMoneyProviders` freely and we restore the + /// original value in `tearDown`. Avoids order-dependent test leakage. + private static let productionAllowlist = ChargeViewModel.supportedMobileMoneyProviders + override func setUpWithError() throws { try super.setUpWithError() + ChargeViewModel.supportedMobileMoneyProviders = Self.productionAllowlist mockRepo = MockChargeRepository() serviceUnderTest = ChargeViewModel(accessCode: "access_code_test", repository: mockRepo) } + override func tearDownWithError() throws { + ChargeViewModel.supportedMobileMoneyProviders = Self.productionAllowlist + try super.tearDownWithError() + } + func testVerifyAccessCodeSetsViewStateAsCardDetailsWhenSuccessful() async { let cardOnlyAccessCode = VerifyAccessCode(amount: 10000, currency: "USD", @@ -23,19 +35,19 @@ final class ChargeViewModelTests: PSTestCase { reference: "test_reference", channelOptions: nil) mockRepo.expectedVerifyAccessCode = cardOnlyAccessCode - await serviceUnderTest.verifyAccessCodeAndProceedWithCard() + await serviceUnderTest.verifyAccessCodeAndProceed() XCTAssertEqual(serviceUnderTest.transactionState, .payment(type: .card(transactionInformation: cardOnlyAccessCode))) } func testVerifyAccessCodeSetsViewStateAsErrorWhenUnsuccessful() async { mockRepo.expectedErrorResponse = ChargeError.generic - await serviceUnderTest.verifyAccessCodeAndProceedWithCard() + await serviceUnderTest.verifyAccessCodeAndProceed() XCTAssertEqual(serviceUnderTest.transactionState, .error(.generic)) } func testVerifyAccessCodeSetsViewStateAsErrorWhenCardIsNotASupportedPaymentChannel() async { - let expectedMessage = "Card payments are not supported. " + + let expectedMessage = "No supported payment methods. " + "Please reach out to your merchant for further information" mockRepo.expectedVerifyAccessCode = .init(amount: 10000, currency: "USD", @@ -45,11 +57,108 @@ final class ChargeViewModelTests: PSTestCase { merchantName: "Test Merchant", publicEncryptionKey: "test_encryption_key", reference: "test_reference", channelOptions: .example) - await serviceUnderTest.verifyAccessCodeAndProceedWithCard() + await serviceUnderTest.verifyAccessCodeAndProceed() XCTAssertEqual(serviceUnderTest.transactionState, .error(.init(message: expectedMessage))) } + // MARK: - Resolver and auto-route + + func testAutoRoutesToMobileMoneyWhenCardUnsupportedAndExactlyOneAllowlistedProvider() async { + ChargeViewModel.supportedMobileMoneyProviders = ["MPESA"] + let response = VerifyAccessCode.with(channels: [.mobileMoney], + mobileMoney: [.mpesaFixture]) + mockRepo.expectedVerifyAccessCode = response + + await serviceUnderTest.verifyAccessCodeAndProceed() + + XCTAssertEqual(serviceUnderTest.transactionState, + .payment(type: .mobileMoney(transactionInformation: response, + provider: .mpesaFixture))) + } + + func testShowsChannelSelectionWhenMultipleMobileMoneyProvidersAndNoCard() async { + ChargeViewModel.supportedMobileMoneyProviders = nil // accept everything + let response = VerifyAccessCode.with(channels: [.mobileMoney], + mobileMoney: [.mtnFixture, .vodafoneFixture]) + mockRepo.expectedVerifyAccessCode = response + + await serviceUnderTest.verifyAccessCodeAndProceed() + + XCTAssertEqual(serviceUnderTest.transactionState, + .channelSelection(transactionInformation: response, + supportedChannels: [.mobileMoney(.mtnFixture), + .mobileMoney(.vodafoneFixture)])) + } + + func testShowsChannelSelectionWhenCardAndMobileMoneyBothSupported() async { + ChargeViewModel.supportedMobileMoneyProviders = ["MPESA"] + let response = VerifyAccessCode.with(channels: [.card, .mobileMoney], + mobileMoney: [.mpesaFixture]) + mockRepo.expectedVerifyAccessCode = response + + await serviceUnderTest.verifyAccessCodeAndProceed() + + XCTAssertEqual(serviceUnderTest.transactionState, + .channelSelection(transactionInformation: response, + supportedChannels: [.card, + .mobileMoney(.mpesaFixture)])) + } + + func testAllowlistFiltersOutUnknownProvidersAndResultsInError() async { + ChargeViewModel.supportedMobileMoneyProviders = ["MPESA"] + let response = VerifyAccessCode.with(channels: [.mobileMoney], + mobileMoney: [.mtnFixture, .vodafoneFixture]) + mockRepo.expectedVerifyAccessCode = response + + await serviceUnderTest.verifyAccessCodeAndProceed() + + let expectedMessage = "No supported payment methods. " + + "Please reach out to your merchant for further information" + XCTAssertEqual(serviceUnderTest.transactionState, + .error(.init(message: expectedMessage))) + } + + func testAllowlistGatesAutoRouteWhenMerchantHasMoreProvidersThanSdkSupports() async { + ChargeViewModel.supportedMobileMoneyProviders = ["MPESA"] + let response = VerifyAccessCode.with(channels: [.mobileMoney], + mobileMoney: [.mpesaFixture, .mtnFixture]) + mockRepo.expectedVerifyAccessCode = response + + await serviceUnderTest.verifyAccessCodeAndProceed() + + XCTAssertEqual(serviceUnderTest.transactionState, + .payment(type: .mobileMoney(transactionInformation: response, + provider: .mpesaFixture))) + } + + func testNilAllowlistAcceptsEveryMobileMoneyProvider() async { + ChargeViewModel.supportedMobileMoneyProviders = nil + let response = VerifyAccessCode.with(channels: [.mobileMoney], + mobileMoney: [.mtnFixture, .vodafoneFixture]) + mockRepo.expectedVerifyAccessCode = response + + await serviceUnderTest.verifyAccessCodeAndProceed() + + XCTAssertEqual(serviceUnderTest.transactionState, + .channelSelection(transactionInformation: response, + supportedChannels: [.mobileMoney(.mtnFixture), + .mobileMoney(.vodafoneFixture)])) + } + + func testTransactionDetailsIsSetAfterResolvingMobileMoneyAutoRoute() async { + // Regression guard: pre-PR-2 the MM branch never set `transactionDetails`, + // so downstream UI checks (inTestMode, chargeCancelled) saw nil. + ChargeViewModel.supportedMobileMoneyProviders = ["MPESA"] + let response = VerifyAccessCode.with(channels: [.mobileMoney], + mobileMoney: [.mpesaFixture]) + mockRepo.expectedVerifyAccessCode = response + + await serviceUnderTest.verifyAccessCodeAndProceed() + + XCTAssertEqual(serviceUnderTest.transactionDetails, response) + } + func testViewShouldBeCenteredForSpecifiedStates() { serviceUnderTest.transactionState = .loading() XCTAssertFalse(serviceUnderTest.centerView) @@ -107,6 +216,8 @@ extension ChargeState: Equatable { return firstAmount == secondAmount && firstMerchant == secondMerchant && firstDetails == secondDetails case (.payment(let first), .payment(let second)): return first == second + case (.channelSelection(let firstInfo, let firstChannels), .channelSelection(let secondInfo, let secondChannels)): + return firstInfo == secondInfo && firstChannels == secondChannels case (.error(let first), .error(let second)): return first.localizedDescription == second.localizedDescription default: @@ -121,3 +232,37 @@ extension ChargeCompletionDetails: Equatable { lhs.reference == rhs.reference } } + +// MARK: - Test fixtures + +private extension MobileMoneyChannel { + static let mpesaFixture = MobileMoneyChannel(key: "MPESA", + value: "M-PESA", + isNew: true, + phoneNumberRegex: "") + static let mtnFixture = MobileMoneyChannel(key: "MTN", + value: "MTN", + isNew: false, + phoneNumberRegex: "") + static let vodafoneFixture = MobileMoneyChannel(key: "VOD", + value: "Vodafone", + isNew: false, + phoneNumberRegex: "") +} + +private extension VerifyAccessCode { + /// Compact helper for building `VerifyAccessCode` fixtures for resolver tests. + /// Most fields are irrelevant to channel resolution and get sensible defaults. + static func with(channels: [PaystackCore.Channel], + mobileMoney: [MobileMoneyChannel]? = nil) -> Self { + VerifyAccessCode(amount: 10000, + currency: "USD", + accessCode: "test_access", + paymentChannels: channels, + domain: .test, + merchantName: "Test Merchant", + publicEncryptionKey: "test_encryption_key", + reference: "test_reference", + channelOptions: mobileMoney.map { PaystackUI.ChannelOptions(mobileMoney: $0) }) + } +} diff --git a/Tests/PaystackSDKTests/UI/Charge/MPesaCharge/MPesaChrageViewModelTests.swift b/Tests/PaystackSDKTests/UI/Charge/MPesaCharge/MPesaChrageViewModelTests.swift index da143d5..4c9895f 100644 --- a/Tests/PaystackSDKTests/UI/Charge/MPesaCharge/MPesaChrageViewModelTests.swift +++ b/Tests/PaystackSDKTests/UI/Charge/MPesaCharge/MPesaChrageViewModelTests.swift @@ -14,6 +14,7 @@ final class MPesaChrageViewModelTests: XCTestCase { mockRepository = MockChargeMobileMoneyRepository() serviceUnderTest = MPesaChrageViewModel(chargeCardContainer: mockChargeContainer, transactionDetails: .example, + provider: .example, repository: mockRepository) } @@ -51,6 +52,7 @@ final class MPesaChrageViewModelTests: XCTestCase { channelOptions: .example) serviceUnderTest = MPesaChrageViewModel(chargeCardContainer: mockChargeContainer, transactionDetails: transactionDetails, + provider: .example, repository: mockRepository) mockRepository.expectedMobileMoneyTransaction = .mPesaExample @@ -89,26 +91,21 @@ final class MPesaChrageViewModelTests: XCTestCase { XCTAssertEqual(mockRepository.chargeMobileMoneySubmitted.transactionId, "0") } - func testSubmitPhoneNumberWithMissingChannelOptionsDefaultsToEmptyProvider() async { - let transactionDetails = VerifyAccessCode(amount: 10000, - currency: "USD", - accessCode: "test_access", - paymentChannels: [.mobileMoney], - domain: .live, - merchantName: "Test Merchant", - publicEncryptionKey: "test_encryption_key", - reference: "test_reference", - transactionId: 1, - channelOptions: nil) + func testSubmitPhoneNumberForwardsTheInjectedProviderKeyEvenWhenNotMPesa() async { + let mtnProvider = MobileMoneyChannel(key: "MTN", + value: "MTN", + isNew: false, + phoneNumberRegex: "") serviceUnderTest = MPesaChrageViewModel(chargeCardContainer: mockChargeContainer, - transactionDetails: transactionDetails, + transactionDetails: .example, + provider: mtnProvider, repository: mockRepository) mockRepository.expectedMobileMoneyTransaction = .mPesaExample serviceUnderTest.phoneNumber = "0703362111" await serviceUnderTest.submitPhoneNumber() - XCTAssertEqual(mockRepository.chargeMobileMoneySubmitted.provider, "") + XCTAssertEqual(mockRepository.chargeMobileMoneySubmitted.provider, "MTN") } func testSubmitPhoneNumberOnSuccessSetsStateToProcessTransaction() async {