diff --git a/android/build.gradle b/android/build.gradle index 6b42efd43..72e28df49 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -128,7 +128,7 @@ dependencies { // noinspection GradleDynamicVersion api 'com.facebook.react:react-native:+' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'com.github.stripe:stripe-android:96fd89ead5' + implementation 'com.github.stripe:stripe-android:cc1dc067e9' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' diff --git a/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt b/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt index 5d4115be8..bd36bf6a7 100644 --- a/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt +++ b/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt @@ -15,6 +15,7 @@ class PaymentMethodCreateParamsFactory(private val clientSecret: String, private PaymentMethod.Type.Alipay -> createAlipayPaymentConfirmParams() PaymentMethod.Type.Sofort -> createSofortPaymentConfirmParams() PaymentMethod.Type.Bancontact -> createBancontactPaymentConfirmParams() + PaymentMethod.Type.Oxxo -> createOXXOPaymentConfirmParams() PaymentMethod.Type.Giropay -> createGiropayPaymentConfirmParams() PaymentMethod.Type.Eps -> createEPSPaymentConfirmParams() PaymentMethod.Type.GrabPay -> createGrabPayPaymentConfirmParams() @@ -253,6 +254,24 @@ class PaymentMethodCreateParamsFactory(private val clientSecret: String, private ) } + @Throws(PaymentMethodCreateParamsException::class) + private fun createOXXOPaymentConfirmParams(): ConfirmPaymentIntentParams { + val billingDetails = billingDetailsParams?.let { it } ?: run { + throw PaymentMethodCreateParamsException("You must provide billing details") + } + if (urlScheme == null) { + throw PaymentMethodCreateParamsException("You must provide urlScheme") + } + val params = PaymentMethodCreateParams.createOxxo(billingDetails) + + return ConfirmPaymentIntentParams + .createWithPaymentMethodCreateParams( + paymentMethodCreateParams = params, + clientSecret = clientSecret, + returnUrl = mapToReturnURL(urlScheme) + ) + } + @Throws(PaymentMethodCreateParamsException::class) private fun createEPSPaymentConfirmParams(): ConfirmPaymentIntentParams { val billingDetails = billingDetailsParams?.let { it } ?: run { diff --git a/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt b/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt index a71110e4e..23bd36a85 100644 --- a/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt +++ b/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt @@ -61,6 +61,16 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ confirmPromise?.resolve(mapFromPaymentIntentResult(paymentIntent)) handleCardActionPromise?.resolve(mapFromPaymentIntentResult(paymentIntent)) } + StripeIntent.Status.RequiresAction -> { + if (isPaymentIntentNextActionVoucherBased(paymentIntent.nextActionType)) { + confirmPromise?.resolve(mapFromPaymentIntentResult(paymentIntent)) + handleCardActionPromise?.resolve(mapFromPaymentIntentResult(paymentIntent)) + } else { + val errorMessage = paymentIntent.lastPaymentError?.message.orEmpty() + confirmPromise?.reject(ConfirmPaymentErrorType.Canceled.toString(), errorMessage) + handleCardActionPromise?.reject(NextPaymentActionErrorType.Canceled.toString(), errorMessage) + } + } StripeIntent.Status.RequiresPaymentMethod -> { val errorMessage = paymentIntent.lastPaymentError?.message.orEmpty() confirmPromise?.reject(ConfirmPaymentErrorType.Failed.toString(), errorMessage) @@ -123,6 +133,16 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ ) } + /// Check paymentIntent.nextAction is voucher-based payment method. + /// If it's voucher-based, the paymentIntent status stays in requiresAction until the voucher is paid or expired. + /// Currently only OXXO payment is voucher-based. + private fun isPaymentIntentNextActionVoucherBased(nextAction: StripeIntent.NextActionType?): Boolean { + nextAction?.let { + return it == StripeIntent.NextActionType.DisplayOxxoDetails + } + return false + } + @ReactMethod fun initialise(params: ReadableMap) { val publishableKey = getValOr(params,"publishableKey", null) as String diff --git a/docs/oxxo/accept-a-payment.md b/docs/oxxo/accept-a-payment.md new file mode 100644 index 000000000..11fdac22e --- /dev/null +++ b/docs/oxxo/accept-a-payment.md @@ -0,0 +1,191 @@ +# OXXO payments + +Use the Payment Intents and Payment Methods APIs to accept OXXO, a common payment method in Mexico. + +## 1. Setup Stripe + +The React Native SDK is open source and fully documented. Under the hood it uses native Android and iOS SDKs. + +To install the SDK run the following command in your terminal: + +```sh +yarn add stripe-react-native +or +npm install stripe-react-native +``` + +For iOS you will have to run `pod install` inside `ios` directory in order to install needed native dependencies. Android won't require any additional steps. + +Configure the SDK with your Stripe [publishable key](https://dashboard.stripe.com/account/apikeys) so that it can make requests to the Stripe API. In order to do that use `StripeProvider` component in the root component of your application. + +```tsx +import { StripeProvider } from 'stripe-react-native'; + +function App() { + return ( + + // Your app code here + + ); +} +``` + +## 2. Create a PaymentIntent + +### Server side + +Stripe uses a PaymentIntent object to represent your intent to collect payment from a customer, tracking your charge attempts and payment state changes throughout the process. + +### Client side + +On the client, request a PaymentIntent from your server and store its client secret. + +```tsx +const fetchPaymentIntentClientSecret = async () => { + const response = await fetch(`${API_URL}/create-payment-intent`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + currency: 'mxn', + items: [{ id: 'id' }], + payment_method_types: ['oxxo'], + }), + }); + const { clientSecret, error } = await response.json(); + + return { clientSecret, error }; +}; +``` + +## 3. Collect payment method details + +In your app, collect your customer’s full name and email address. + +```tsx +export default function OxxoPaymentScreen() { + const [name, setName] = useState(); + const [email, setEmail] = useState(); + + const handlePayPress = async () => { + // ... + }; + + return ( + + setName(value.nativeEvent.text)} + /> + setEmail(value.nativeEvent.text)} + /> + + ); +} +``` + +## 4. Submit the payment to Stripe + +Retrieve the client secret from the PaymentIntent you created in step 2 and call `confirmPayment` method. This presents a webview where the customer can complete the payment on their bank’s website or app. Afterwards, the promise will be resolved with the result of the payment. + +The Stripe React Native SDK specifies `safepay/` as the host for the return URL for bank redirect methods. After the customer completes their payment with Bancontact, your app will be opened with `myapp://safepay/` where `myapp` is your custom URL scheme. + +```tsx +export default function OxxoPaymentScreen() { + const [name, setName] = useState(); + const [email, setEmail] = useState(); + + const handlePayPress = async () => { + const billingDetails: PaymentMethodCreateParams.BillingDetails = { + name, + email, + }; + }; + + const { error, paymentIntent } = await confirmPayment(clientSecret, { + type: 'Oxxo', + billingDetails, + }); + + if (error) { + Alert.alert(`Error code: ${error.code}`, error.message); + console.log('Payment confirmation error', error.message); + } else if (paymentIntent) { + if (paymentIntent.status === PaymentIntents.Status.RequiresAction) { + Alert.alert( + 'Success', + `The OXXO voucher was created successfully. Awaiting payment from customer.` + ); + } else { + Alert.alert('Payment intent status:', paymentIntent.status); + } + } +}; + + return ( + + setName(value.nativeEvent.text)} + /> + setEmail(value.nativeEvent.text)} + /> + + ); +} +``` + +## 5. Handle deep linking + +To handle deep linking for bank redirect and wallet payment methods, your app will need to register a custom url scheme. If you're using Expo, [set your scheme](https://docs.expo.io/guides/linking/#in-a-standalone-app) in the `app.json` file. + +Otherwise, follow the React Native Linking module [docs](https://reactnative.dev/docs/linking) to configure deep linking. For more information on native URL schemes, refer to the native [Android](https://developer.android.com/training/app-links/deep-linking) and [iOS](https://developer.apple.com/documentation/xcode/allowing_apps_and_websites_to_link_to_your_content/defining_a_custom_url_scheme_for_your_app) docs. + +Once your scheme is configured, you can specify a callback to handle the URLs: + +```tsx +import React, { useEffect } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { useStripe } from 'stripe-react-native'; + +import { Linking } from 'react-native'; +// For Expo use this import instead: +// import * as Linking from 'expo-linking'; + +export default function HomeScreen() { + const navigation = useNavigation(); + const { handleURLCallback } = useStripe(); + + const handleDeepLink = async () => { + if (url && url.includes(`safepay`)) { + await handleURLCallback(url); + navigation.navigate('PaymentResultScreen', { url }); + } + }; + + useEffect(() => { + const getUrlAsync = async () => { + const initialUrl = await Linking.getInitialURL(); + handleDeepLink(initialUrl); + }; + getUrlAsync(); + + const urlCallback = (event) => { + handleDeepLink(event.url); + }; + + Linking.addEventListener('url', urlCallback); + return () => Linking.removeEventListener('url', urlCallback); + }, []); + + return {/* ... */}; +} +``` + +## 6. Handle post-payment events diff --git a/example/ios/Podfile b/example/ios/Podfile index 6538d2791..b485404d7 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -8,13 +8,13 @@ target 'StripeSdkExample' do use_react_native!(:path => config["reactNativePath"]) pod 'stripe-react-native', :path => '../..' - pod 'Stripe', :git => 'https://github.com/thorsten-stripe/stripe-ios', :branch => 'fix/ideal-mandate-data' + pod 'Stripe', :git => 'https://github.com/thorsten-stripe/stripe-ios', :branch => 'fix/voucher-intent-status' # Enables Flipper. # # Note that if you have use_frameworks! enabled, Flipper will not work and # you should disable these next few lines. - + # Workaround for https://github.com/facebook/react-native/issues/30836 use_flipper!({ 'Flipper-Folly' => '2.3.0' }) post_install do |installer| diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0e54ee658..3e9b75978 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -371,7 +371,7 @@ DEPENDENCIES: - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - - Stripe (from `https://github.com/thorsten-stripe/stripe-ios`, branch `fix/ideal-mandate-data`) + - Stripe (from `https://github.com/thorsten-stripe/stripe-ios`, branch `fix/voucher-intent-status`) - stripe-react-native (from `../..`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -454,7 +454,7 @@ EXTERNAL SOURCES: RNScreens: :path: "../node_modules/react-native-screens" Stripe: - :branch: fix/ideal-mandate-data + :branch: fix/voucher-intent-status :git: https://github.com/thorsten-stripe/stripe-ios stripe-react-native: :path: "../.." @@ -463,7 +463,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: Stripe: - :commit: a1b2e5b483b4f4ddbff21746afc0eec8a44891dd + :commit: 3f3be7230fb95002851a124dd3e978494450da30 :git: https://github.com/thorsten-stripe/stripe-ios SPEC CHECKSUMS: @@ -514,6 +514,6 @@ SPEC CHECKSUMS: Yoga: 4bd86afe9883422a7c4028c00e34790f560923d6 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 4b2c8c8456cd09b6f08da3604c6082e9a4e4ee81 +PODFILE CHECKSUM: 0d9b057f12ef1795e2beda2c4c7812e048942979 COCOAPODS: 1.10.1 diff --git a/example/src/App.tsx b/example/src/App.tsx index 28e8c6289..fff14a0b2 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -21,6 +21,7 @@ import SofortSetupFuturePaymentScreen from './screens/SofortSetupFuturePaymentSc import FPXPaymentScreen from './screens/FPXPaymentScreen'; import BancontactPaymentScreen from './screens/BancontactPaymentScreen'; import BancontactSetupFuturePaymentScreen from './screens/BancontactSetupFuturePaymentScreen'; +import OxxoPaymentScreen from './screens/OxxoPaymentScreen'; import GiropayPaymentScreen from './screens/GiropayPaymentScreen'; import EPSPaymentScreen from './screens/EPSPaymentScreen'; import GrabPayPaymentScreen from './screens/GrabPayPaymentScreen'; @@ -144,6 +145,10 @@ export default function App() { name="BancontactSetupFuturePaymentScreen" component={BancontactSetupFuturePaymentScreen} /> + + +