Skip to content

Commit

Permalink
Merge pull request #139 from stripe/feat/oxxo-payment
Browse files Browse the repository at this point in the history
Oxxo payment
  • Loading branch information
thorsten-stripe authored Apr 6, 2021
2 parents 9238180 + 693929c commit 43090b2
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 7 deletions.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
191 changes: 191 additions & 0 deletions docs/oxxo/accept-a-payment.md
Original file line number Diff line number Diff line change
@@ -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 (
<StripeProvider publishableKey="pk_test_51Ho4m5A51v44wNexXNFEg0MSAjZUzllhhJwiFmAmJ4tzbvsvuEgcMCaPEkgK7RpXO1YI5okHP08IUfJ6YS7ulqzk00O2I0D1rT">
// Your app code here
</StripeProvider>
);
}
```

## 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 (
<Screen>
<TextInput
placeholder="Name"
onChange={(value) => setName(value.nativeEvent.text)}
/>
<TextInput
placeholder="E-mail"
onChange={(value) => setEmail(value.nativeEvent.text)}
/>
</Screen>
);
}
```

## 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 (
<Screen>
<TextInput
placeholder="Name"
onChange={(value) => setName(value.nativeEvent.text)}
/>
<TextInput
placeholder="E-mail"
onChange={(value) => setEmail(value.nativeEvent.text)}
/>
</Screen>
);
}
```

## 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 <Screen>{/* ... */}</Screen>;
}
```

## 6. Handle post-payment events
4 changes: 2 additions & 2 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down Expand Up @@ -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: "../.."
Expand All @@ -463,7 +463,7 @@ EXTERNAL SOURCES:

CHECKOUT OPTIONS:
Stripe:
:commit: a1b2e5b483b4f4ddbff21746afc0eec8a44891dd
:commit: 3f3be7230fb95002851a124dd3e978494450da30
:git: https://github.com/thorsten-stripe/stripe-ios

SPEC CHECKSUMS:
Expand Down Expand Up @@ -514,6 +514,6 @@ SPEC CHECKSUMS:
Yoga: 4bd86afe9883422a7c4028c00e34790f560923d6
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: 4b2c8c8456cd09b6f08da3604c6082e9a4e4ee81
PODFILE CHECKSUM: 0d9b057f12ef1795e2beda2c4c7812e048942979

COCOAPODS: 1.10.1
5 changes: 5 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -144,6 +145,10 @@ export default function App() {
name="BancontactSetupFuturePaymentScreen"
component={BancontactSetupFuturePaymentScreen}
/>
<Stack.Screen
name="OxxoPaymentScreen"
component={OxxoPaymentScreen}
/>
<Stack.Screen
name="GiropayPaymentScreen"
component={GiropayPaymentScreen}
Expand Down
9 changes: 9 additions & 0 deletions example/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ export default function HomeScreen() {
/>
</View>

<View style={styles.buttonContainer}>
<Button
title="Oxxo Payment"
onPress={() => {
navigation.navigate('OxxoPaymentScreen');
}}
/>
</View>

<View style={styles.buttonContainer}>
<Button
title="EPS Payment"
Expand Down
Loading

0 comments on commit 43090b2

Please sign in to comment.