Skip to content
Draft
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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## Unreleased
**Bug fixes**
* [Fixed] Android and iOS: `confirmPayment` and `confirmSetupIntent` now correctly set `moto=true` for card payments when the SDK is initialized with a user key (`uk_`) and a saved payment method ID is used.

## 0.73.0 - 2026-08-04
**Changes**
* Updated Stripe iOS SDK from 26.4.1 to 26.5.0.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.reactnativestripesdk

import android.annotation.SuppressLint
import com.facebook.react.bridge.ReadableMap
import com.reactnativestripesdk.utils.getBooleanOr
import com.reactnativestripesdk.utils.getValOr
Expand All @@ -22,6 +23,7 @@ class PaymentMethodCreateParamsFactory(
private val options: ReadableMap?,
private val cardFieldView: CardFieldView?,
private val cardFormView: CardFormView?,
private val publishableKey: String = "",
) {
private val billingDetailsParams =
mapToBillingDetails(
Expand Down Expand Up @@ -357,6 +359,7 @@ class PaymentMethodCreateParamsFactory(
return PaymentMethodCreateParams.create(cardParams, billingDetailsParams)
}

@SuppressLint("RestrictedApi")
@Throws(PaymentMethodCreateParamsException::class)
private fun createCardStripeIntentParams(
clientSecret: String,
Expand All @@ -367,8 +370,12 @@ class PaymentMethodCreateParamsFactory(

if (paymentMethodId != null) {
val cvc = getValOr(paymentMethodData, "cvc", null)
val paymentMethodOptionParams =
if (cvc != null) PaymentMethodOptionsParams.Card(cvc) else null
val isMoto = publishableKey.startsWith("uk_")
val paymentMethodOptionParams = when {
isMoto -> PaymentMethodOptionsParams.Card(cvc = cvc, moto = true)
cvc != null -> PaymentMethodOptionsParams.Card(cvc)
else -> null
}

return (
if (isPaymentIntent) {
Expand All @@ -379,7 +386,11 @@ class PaymentMethodCreateParamsFactory(
setupFutureUsage = setupFutureUsage,
)
} else {
ConfirmSetupIntentParams.create(paymentMethodId, clientSecret)
ConfirmSetupIntentParams(
clientSecret = clientSecret,
paymentMethodId = paymentMethodId,
paymentMethodOptions = paymentMethodOptionParams,
)
}
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ class StripeSdkModule(
}
val paymentMethodData = data.getMap("paymentMethodData")
val factory =
PaymentMethodCreateParamsFactory(paymentMethodData, options, cardFieldView, cardFormView)
PaymentMethodCreateParamsFactory(paymentMethodData, options, cardFieldView, cardFormView, publishableKey)
try {
val paymentMethodCreateParams = factory.createPaymentMethodParams(paymentMethodType)
stripe.createPaymentMethod(
Expand Down Expand Up @@ -671,7 +671,7 @@ class StripeSdkModule(
// }

val factory =
PaymentMethodCreateParamsFactory(paymentMethodData, options, cardFieldView, cardFormView)
PaymentMethodCreateParamsFactory(paymentMethodData, options, cardFieldView, cardFormView, publishableKey)

try {
val confirmParams =
Expand Down Expand Up @@ -758,6 +758,7 @@ class StripeSdkModule(
options,
cardFieldView,
cardFormView,
publishableKey,
)

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.reactnativestripesdk

import android.annotation.SuppressLint
import com.reactnativestripesdk.utils.readableMapOf
import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.ConfirmSetupIntentParams
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodOptionsParams
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@SuppressLint("RestrictedApi")
@RunWith(RobolectricTestRunner::class)
class PaymentMethodCreateParamsFactoryTest {

private val clientSecret = "pi_test_secret_fake"
private val savedPaymentMethodId = "pm_test_fake"

// ============================================
// confirmPayment (PaymentIntent) + saved card
// ============================================

@Test
fun createParams_userKey_paymentIntent_savedCard_setsMoto() {
val factory = factoryWithSavedCard(publishableKey = "uk_test_abc123")

val params = factory.createParams(clientSecret, PaymentMethod.Type.Card, isPaymentIntent = true)
val confirmParams = params as ConfirmPaymentIntentParams
val cardOptions = confirmParams.paymentMethodOptions as? PaymentMethodOptionsParams.Card

assertNotNull("paymentMethodOptions should be set for uk_ key", cardOptions)
val cardParamMap = cardOptions!!.toParamMap()["card"] as? Map<*, *>
assertNotNull("card params should be present", cardParamMap)
assertEquals("moto should be true", true, cardParamMap!!["moto"])
assertEquals(savedPaymentMethodId, confirmParams.paymentMethodId)
}

@Test
fun createParams_publishableKey_paymentIntent_savedCard_doesNotSetMoto() {
val factory = factoryWithSavedCard(publishableKey = "pk_test_abc123")

val params = factory.createParams(clientSecret, PaymentMethod.Type.Card, isPaymentIntent = true)
val confirmParams = params as ConfirmPaymentIntentParams

// No options for pk_ key when no CVC
assertNull("paymentMethodOptions should be null for pk_ key without CVC", confirmParams.paymentMethodOptions)
}

@Test
fun createParams_userKey_paymentIntent_savedCard_withCvc_setsMotoAndPreservesCvc() {
val factory = factoryWithSavedCard(publishableKey = "uk_test_abc123", cvc = "123")

val params = factory.createParams(clientSecret, PaymentMethod.Type.Card, isPaymentIntent = true)
val confirmParams = params as ConfirmPaymentIntentParams
val cardOptions = confirmParams.paymentMethodOptions as? PaymentMethodOptionsParams.Card

assertNotNull(cardOptions)
val cardParamMap = cardOptions!!.toParamMap()["card"] as? Map<*, *>
assertNotNull("card params should be present", cardParamMap)
assertEquals("moto should be true", true, cardParamMap!!["moto"])
assertEquals("CVC should be preserved", "123", cardParamMap["cvc"])
}

@Test
fun createParams_publishableKey_paymentIntent_savedCard_withCvc_setsOnlyCvc() {
val factory = factoryWithSavedCard(publishableKey = "pk_test_abc123", cvc = "123")

val params = factory.createParams(clientSecret, PaymentMethod.Type.Card, isPaymentIntent = true)
val confirmParams = params as ConfirmPaymentIntentParams
val cardOptions = confirmParams.paymentMethodOptions as? PaymentMethodOptionsParams.Card

assertNotNull(cardOptions)
val cardParamMap = cardOptions!!.toParamMap()["card"] as? Map<*, *>
assertNotNull("card params should be present", cardParamMap)
assertNull("moto should not be set for pk_ key", cardParamMap!!["moto"])
assertEquals("CVC should be set", "123", cardParamMap["cvc"])
}

// ============================================
// confirmSetupIntent (SetupIntent) + saved card
// ============================================

@Test
fun createParams_userKey_setupIntent_savedCard_setsMoto() {
val factory = factoryWithSavedCard(publishableKey = "uk_test_abc123")

val params = factory.createParams(clientSecret, PaymentMethod.Type.Card, isPaymentIntent = false)
val confirmParams = params as ConfirmSetupIntentParams
val cardOptions = confirmParams.paymentMethodOptions as? PaymentMethodOptionsParams.Card

assertNotNull("paymentMethodOptions should be set for uk_ key", cardOptions)
val cardParamMap = cardOptions!!.toParamMap()["card"] as? Map<*, *>
assertNotNull("card params should be present", cardParamMap)
assertEquals("moto should be true", true, cardParamMap!!["moto"])
}

@Test
fun createParams_publishableKey_setupIntent_savedCard_doesNotSetMoto() {
val factory = factoryWithSavedCard(publishableKey = "pk_test_abc123")

val params = factory.createParams(clientSecret, PaymentMethod.Type.Card, isPaymentIntent = false)
val confirmParams = params as ConfirmSetupIntentParams

assertNull("paymentMethodOptions should be null for pk_ key without CVC", confirmParams.paymentMethodOptions)
}

// ============================================
// Helpers
// ============================================

private fun factoryWithSavedCard(
publishableKey: String,
cvc: String? = null,
): PaymentMethodCreateParamsFactory {
val pairs = mutableListOf<Pair<String, Any?>>("paymentMethodId" to savedPaymentMethodId)
if (cvc != null) pairs.add("cvc" to cvc)
return PaymentMethodCreateParamsFactory(
paymentMethodData = readableMapOf(*pairs.toTypedArray()),
options = readableMapOf(),
cardFieldView = null,
cardFormView = null,
publishableKey = publishableKey,
)
}
}
11 changes: 9 additions & 2 deletions ios/PaymentMethodFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ class PaymentMethodFactory {
var cardFieldView: CardFieldView?
var cardFormView: CardFormView?
var metadata: [String: String]?
var publishableKey: String

init(paymentMethodData: NSDictionary?, options: NSDictionary, cardFieldView: CardFieldView?, cardFormView: CardFormView?) {
init(paymentMethodData: NSDictionary?, options: NSDictionary, cardFieldView: CardFieldView?, cardFormView: CardFormView?, publishableKey: String = "") {
self.paymentMethodData = paymentMethodData
self.billingDetailsParams = Mappers.mapToBillingDetails(billingDetails: paymentMethodData?["billingDetails"] as? NSDictionary)
self.paymentMethodOptions = options
self.cardFieldView = cardFieldView
self.cardFormView = cardFormView
self.metadata = paymentMethodData?["metadata"] as? [String: String]
self.publishableKey = publishableKey
}

func createParams(paymentMethodType: STPPaymentMethodType) throws -> STPPaymentMethodParams? {
Expand Down Expand Up @@ -216,12 +218,17 @@ class PaymentMethodFactory {

private func createCardPaymentMethodOptions() -> STPConfirmPaymentMethodOptions? {
let cvc = paymentMethodData?["cvc"] as? String
guard cvc != nil else {
let isMoto = publishableKey.hasPrefix("uk_")

guard cvc != nil || isMoto else {
return nil
}

let cardOptions = STPConfirmCardOptions()
cardOptions.cvc = cvc
if isMoto {
cardOptions.additionalAPIParameters["moto"] = true
}
let paymentMethodOptions = STPConfirmPaymentMethodOptions()
paymentMethodOptions.cardOptions = cardOptions

Expand Down
11 changes: 8 additions & 3 deletions ios/StripeSdkImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,13 @@ public class StripeSdkImpl: NSObject, UIAdaptivePresentationControllerDelegate {
if paymentMethodType == .USBankAccount && paymentMethodData == nil {
return STPSetupIntentConfirmParams(clientSecret: setupIntentClientSecret, paymentMethodType: .USBankAccount)
} else {
let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView)
let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView, publishableKey: STPAPIClient.shared.publishableKey ?? "")
let parameters = STPSetupIntentConfirmParams(clientSecret: setupIntentClientSecret)

if paymentMethodType == .card && STPAPIClient.shared.publishableKey?.hasPrefix("uk_") == true {
parameters.additionalAPIParameters["payment_method_options"] = ["card": ["moto": true]]
}

if let paymentMethodId = paymentMethodData?["paymentMethodId"] as? String {
parameters.paymentMethodID = paymentMethodId
} else {
Expand Down Expand Up @@ -621,7 +625,8 @@ public class StripeSdkImpl: NSObject, UIAdaptivePresentationControllerDelegate {
paymentMethodData: params["paymentMethodData"] as? NSDictionary,
options: options,
cardFieldView: cardFieldView,
cardFormView: cardFormView
cardFormView: cardFormView,
publishableKey: STPAPIClient.shared.publishableKey ?? ""
)
do {
paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType)
Expand Down Expand Up @@ -964,7 +969,7 @@ public class StripeSdkImpl: NSObject, UIAdaptivePresentationControllerDelegate {
return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret, paymentMethodType: .USBankAccount)
} else {
guard let paymentMethodType = paymentMethodType else { return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret) }
let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView)
let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView, publishableKey: STPAPIClient.shared.publishableKey ?? "")
let paymentMethodId = paymentMethodData?["paymentMethodId"] as? String
let parameters = STPPaymentIntentParams(clientSecret: paymentIntentClientSecret)

Expand Down
109 changes: 109 additions & 0 deletions ios/Tests/PaymentMethodFactoryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import Stripe
@testable import stripe_react_native
import XCTest

class PaymentMethodFactoryTests: XCTestCase {

// MARK: - createCardPaymentMethodOptions: moto injection

func test_createOptions_userKey_noCard_setsMoto() throws {
let factory = makeFactory(publishableKey: "uk_test_abc123")

let options = try factory.createOptions(paymentMethodType: .card)

XCTAssertNotNil(options, "Options should be set for uk_ key")
XCTAssertEqual(
options?.cardOptions?.additionalAPIParameters["moto"] as? Bool,
true,
"moto should be true for uk_ key"
)
}

func test_createOptions_publishableKey_noCard_noCvc_returnsNil() throws {
let factory = makeFactory(publishableKey: "pk_test_abc123")

let options = try factory.createOptions(paymentMethodType: .card)

XCTAssertNil(options, "Options should be nil for pk_ key with no CVC")
}

func test_createOptions_userKey_withCvc_setsMotoAndCvc() throws {
let factory = makeFactory(publishableKey: "uk_test_abc123", paymentMethodData: ["cvc": "123"])

let options = try factory.createOptions(paymentMethodType: .card)

XCTAssertNotNil(options)
XCTAssertEqual(
options?.cardOptions?.additionalAPIParameters["moto"] as? Bool,
true,
"moto should be true for uk_ key"
)
XCTAssertEqual(options?.cardOptions?.cvc, "123", "CVC should be preserved")
}

func test_createOptions_publishableKey_withCvc_doesNotSetMoto() throws {
let factory = makeFactory(publishableKey: "pk_test_abc123", paymentMethodData: ["cvc": "123"])

let options = try factory.createOptions(paymentMethodType: .card)

XCTAssertNotNil(options)
XCTAssertNil(
options?.cardOptions?.additionalAPIParameters["moto"],
"moto should not be set for pk_ key"
)
XCTAssertEqual(options?.cardOptions?.cvc, "123", "CVC should be set")
}

func test_createOptions_userKey_savedCard_setsMoto() throws {
// Saved-card confirm passes paymentMethodId but no inline card params
let factory = makeFactory(
publishableKey: "uk_test_abc123",
paymentMethodData: ["paymentMethodId": "pm_test_fake"]
)

let options = try factory.createOptions(paymentMethodType: .card)

XCTAssertNotNil(options, "Options should be set for uk_ key even without CVC")
XCTAssertEqual(
options?.cardOptions?.additionalAPIParameters["moto"] as? Bool,
true,
"moto should be true for uk_ key with saved card"
)
}

func test_createOptions_publishableKey_savedCard_returnsNil() throws {
let factory = makeFactory(
publishableKey: "pk_test_abc123",
paymentMethodData: ["paymentMethodId": "pm_test_fake"]
)

let options = try factory.createOptions(paymentMethodType: .card)

XCTAssertNil(options, "Options should be nil for pk_ key without CVC")
}

// MARK: - Non-card payment types are unaffected

func test_createOptions_userKey_iDEAL_returnsNil() throws {
let factory = makeFactory(publishableKey: "uk_test_abc123")

let options = try factory.createOptions(paymentMethodType: .iDEAL)

XCTAssertNil(options, "Non-card types should not get moto options")
}

// MARK: - Helpers

private func makeFactory(
publishableKey: String,
paymentMethodData: NSDictionary? = nil
) -> PaymentMethodFactory {
return PaymentMethodFactory(
paymentMethodData: paymentMethodData,
options: NSDictionary(),
cardFieldView: nil,
cardFormView: nil,
publishableKey: publishableKey
)
}
}
Loading