From 3ab03a7c438ea4153547a1d026addacd677ab895 Mon Sep 17 00:00:00 2001 From: charliecruzan-stripe <97612659+charliecruzan-stripe@users.noreply.github.com> Date: Wed, 13 Jul 2022 19:11:10 -0400 Subject: [PATCH] feat: add payment method: affirm (#1036) --- .../PaymentMethodCreateParamsFactory.kt | 23 ++++ .../com/reactnativestripesdk/utils/Mappers.kt | 2 + docs/accept-a-payment-multiline-card.md | 2 +- docs/wechat-pay.md | 2 +- e2e/buyNowPayLater.test.ts | 13 ++ e2e/screenObject/HomeScreen.ts | 1 + example/server/index.ts | 27 ++-- example/src/App.tsx | 3 + example/src/screens/ACHPaymentScreen.tsx | 2 +- example/src/screens/AffirmScreen.tsx | 121 ++++++++++++++++++ .../screens/AfterpayClearpayPaymentScreen.tsx | 2 +- example/src/screens/AlipayPaymentScreen.tsx | 2 +- .../src/screens/AuBECSDebitPaymentScreen.tsx | 2 +- .../src/screens/BancontactPaymentScreen.tsx | 2 +- example/src/screens/CVCReCollectionScreen.tsx | 6 +- example/src/screens/EPSPaymentScreen.tsx | 2 +- example/src/screens/FPXPaymentScreen.tsx | 2 +- example/src/screens/GiropayPaymentScreen.tsx | 2 +- example/src/screens/GooglePayScreen.tsx | 2 +- example/src/screens/GrabPayPaymentScreen.tsx | 2 +- example/src/screens/HomeScreen.tsx | 8 ++ example/src/screens/IdealPaymentScreen.tsx | 2 +- example/src/screens/KlarnaPaymentScreen.tsx | 2 +- .../screens/MultilineWebhookPaymentScreen.tsx | 2 +- .../src/screens/NoWebhookPaymentScreen.tsx | 4 +- example/src/screens/OxxoPaymentScreen.tsx | 2 +- example/src/screens/P24PaymentScreen.tsx | 2 +- example/src/screens/SepaPaymentScreen.tsx | 2 +- example/src/screens/SofortPaymentScreen.tsx | 2 +- example/src/screens/WebhookPaymentScreen.tsx | 2 +- ios/Mappers.swift | 2 + ios/PaymentMethodFactory.swift | 9 ++ src/types/PaymentMethod.ts | 12 +- 33 files changed, 234 insertions(+), 37 deletions(-) create mode 100644 example/src/screens/AffirmScreen.tsx diff --git a/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt b/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt index 6e1355154..588b6c18f 100644 --- a/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt +++ b/android/src/main/java/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt @@ -36,6 +36,7 @@ class PaymentMethodCreateParamsFactory( PaymentMethod.Type.Klarna -> createKlarnaParams() PaymentMethod.Type.USBankAccount -> createUSBankAccountParams(paymentMethodData) PaymentMethod.Type.PayPal -> createPayPalParams() + PaymentMethod.Type.Affirm -> createAffirmParams() else -> { throw Exception("This paymentMethodType is not supported yet") } @@ -196,6 +197,11 @@ class PaymentMethodCreateParamsFactory( return PaymentMethodCreateParams.createPayPal(null) } + @Throws(PaymentMethodCreateParamsException::class) + private fun createAffirmParams(): PaymentMethodCreateParams { + return PaymentMethodCreateParams.createAffirm(billingDetailsParams) + } + @Throws(PaymentMethodCreateParamsException::class) fun createParams(clientSecret: String, paymentMethodType: PaymentMethod.Type, isPaymentIntent: Boolean): ConfirmStripeIntentParams { try { @@ -203,6 +209,7 @@ class PaymentMethodCreateParamsFactory( PaymentMethod.Type.Card -> createCardStripeIntentParams(clientSecret, isPaymentIntent) PaymentMethod.Type.USBankAccount -> createUSBankAccountStripeIntentParams(clientSecret, isPaymentIntent) PaymentMethod.Type.PayPal -> createPayPalStripeIntentParams(clientSecret, isPaymentIntent) + PaymentMethod.Type.Affirm -> createAffirmStripeIntentParams(clientSecret, isPaymentIntent) PaymentMethod.Type.Ideal, PaymentMethod.Type.Alipay, @@ -346,6 +353,22 @@ class PaymentMethodCreateParamsFactory( ) } + @Throws(PaymentMethodCreateParamsException::class) + private fun createAffirmStripeIntentParams(clientSecret: String, isPaymentIntent: Boolean): ConfirmStripeIntentParams { + if (!isPaymentIntent) { + throw PaymentMethodCreateParamsException("Affirm is not yet supported through SetupIntents.") + } + + val params = createAffirmParams() + + return ConfirmPaymentIntentParams + .createWithPaymentMethodCreateParams( + paymentMethodCreateParams = params, + clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), + ) + } + @Throws(PaymentMethodCreateParamsException::class) private fun createUSBankAccountParams(params: ReadableMap?): PaymentMethodCreateParams { val accountNumber = getValOr(params, "accountNumber", null) diff --git a/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt b/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt index e2471e106..e794f0e60 100644 --- a/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt +++ b/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt @@ -124,6 +124,7 @@ internal fun mapPaymentMethodType(type: PaymentMethod.Type?): String { PaymentMethod.Type.Klarna -> "Klarna" PaymentMethod.Type.USBankAccount -> "USBankAccount" PaymentMethod.Type.PayPal -> "PayPal" + PaymentMethod.Type.Affirm -> "Affirm" else -> "Unknown" } } @@ -152,6 +153,7 @@ internal fun mapToPaymentMethodType(type: String?): PaymentMethod.Type? { "Klarna" -> PaymentMethod.Type.Klarna "USBankAccount" -> PaymentMethod.Type.USBankAccount "PayPal" -> PaymentMethod.Type.PayPal + "Affirm" -> PaymentMethod.Type.Affirm else -> null } } diff --git a/docs/accept-a-payment-multiline-card.md b/docs/accept-a-payment-multiline-card.md index 0c2aae1b0..1612aab62 100644 --- a/docs/accept-a-payment-multiline-card.md +++ b/docs/accept-a-payment-multiline-card.md @@ -85,7 +85,7 @@ function PaymentScreen() { }, body: JSON.stringify({ currency: 'usd', - items: [{ id: 'id' }], + items: ['id-1'], }), }); const { clientSecret } = await response.json(); diff --git a/docs/wechat-pay.md b/docs/wechat-pay.md index 0956cb76b..77167dff9 100644 --- a/docs/wechat-pay.md +++ b/docs/wechat-pay.md @@ -135,7 +135,7 @@ function PaymentScreen() { }, body: JSON.stringify({ currency: 'usd', - items: [{ id: 'id' }], + items: ['id-1'], }), }); const { clientSecret } = await response.json(); diff --git a/e2e/buyNowPayLater.test.ts b/e2e/buyNowPayLater.test.ts index fff7c72b3..8094ad5a2 100644 --- a/e2e/buyNowPayLater.test.ts +++ b/e2e/buyNowPayLater.test.ts @@ -26,6 +26,19 @@ describe('Payment scenarios with redirects', () => { BasicPaymentScreen.checkStatus(); }); + it('Affirm payment scenario', function () { + this.retries(3); + + homeScreen.goTo('Buy now pay later'); + homeScreen.goTo('Affirm'); + + $('~payment-screen').waitForDisplayed({ timeout: 30000 }); + + BasicPaymentScreen.pay({ email: 'test@stripe.com' }); + BasicPaymentScreen.authorize({ pause: 10000 }); + BasicPaymentScreen.checkStatus(); + }); + it('Opens Klarna webview', function () { this.retries(3); diff --git a/e2e/screenObject/HomeScreen.ts b/e2e/screenObject/HomeScreen.ts index 3d821d0c0..c3c84f6db 100644 --- a/e2e/screenObject/HomeScreen.ts +++ b/e2e/screenObject/HomeScreen.ts @@ -37,6 +37,7 @@ const SCREENS = [ 'WeChat Pay', 'ACH payment', 'ACH setup', + 'Affirm', ]; class HomeScreen { diff --git a/example/server/index.ts b/example/server/index.ts index 2dc8e90a5..f8aadf3aa 100644 --- a/example/server/index.ts +++ b/example/server/index.ts @@ -32,15 +32,20 @@ app.use( ); // tslint:disable-next-line: interface-name -interface Order { - items: object[]; -} +const itemIdToPrice: { [id: string]: number } = { + 'id-1': 1400, + 'id-2': 2000, + 'id-3': 3000, + 'id-4': 4000, + 'id-5': 5000, +}; + +const calculateOrderAmount = (itemIds: string[] = ['id-1']): number => { + const total = itemIds + .map((id) => itemIdToPrice[id]) + .reduce((prev, curr) => prev + curr, 0); -const calculateOrderAmount = (_order?: Order): number => { - // Replace this constant with a calculation of the order's amount. - // Calculate the order total on the server to prevent - // people from directly manipulating the amount on the client. - return 1400; + return total; }; function getKeys(payment_method?: string) { @@ -97,7 +102,7 @@ app.post( client = 'ios', }: { email: string; - items: Order; + items: string[]; currency: string; payment_method_types: string[]; request_three_d_secure: 'any' | 'automatic'; @@ -159,7 +164,7 @@ app.post( request_three_d_secure, email, }: { - items: Order; + items: string[]; currency: string; request_three_d_secure: 'any' | 'automatic'; email: string; @@ -235,7 +240,7 @@ app.post( paymentMethodId?: string; paymentIntentId?: string; cvcToken?: string; - items: Order; + items: string[]; currency: string; useStripeSdk: boolean; email?: string; diff --git a/example/src/App.tsx b/example/src/App.tsx index fada3f0ff..d1d695413 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -38,6 +38,7 @@ import GooglePayScreen from './screens/GooglePayScreen'; import ACHPaymentScreen from './screens/ACHPaymentScreen'; import ACHSetupScreen from './screens/ACHSetupScreen'; import PayPalScreen from './screens/PayPalScreen'; +import AffirmScreen from './screens/AffirmScreen'; const Stack = createNativeStackNavigator(); @@ -77,6 +78,7 @@ export type RootStackParamList = { ACHPaymentScreen: undefined; ACHSetupScreen: undefined; PayPalScreen: undefined; + AffirmScreen: undefined; }; declare global { @@ -218,6 +220,7 @@ export default function App() { + diff --git a/example/src/screens/ACHPaymentScreen.tsx b/example/src/screens/ACHPaymentScreen.tsx index 5d37e5683..c4216da74 100644 --- a/example/src/screens/ACHPaymentScreen.tsx +++ b/example/src/screens/ACHPaymentScreen.tsx @@ -32,7 +32,7 @@ export default function ACHPaymentScreen() { body: JSON.stringify({ email: email, currency: 'usd', - items: [{ id: 'id' }], + items: ['id-1'], payment_method_types: ['us_bank_account'], }), }); diff --git a/example/src/screens/AffirmScreen.tsx b/example/src/screens/AffirmScreen.tsx new file mode 100644 index 000000000..0b69e86c7 --- /dev/null +++ b/example/src/screens/AffirmScreen.tsx @@ -0,0 +1,121 @@ +import type { PaymentMethod } from '@stripe/stripe-react-native'; +import React, { useState } from 'react'; +import { Alert, TextInput, StyleSheet } from 'react-native'; +import { + useConfirmPayment, + createPaymentMethod, +} from '@stripe/stripe-react-native'; +import Button from '../components/Button'; +import PaymentScreen from '../components/PaymentScreen'; +import { API_URL } from '../Config'; +import { colors } from '../colors'; + +export default function AffirmScreen() { + const [email, setEmail] = useState(''); + const { confirmPayment, loading } = useConfirmPayment(); + + const fetchPaymentIntentClientSecret = async () => { + const response = await fetch(`${API_URL}/create-payment-intent`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + currency: 'usd', + items: ['id-5'], + payment_method_types: ['affirm'], + }), + }); + const { clientSecret, error } = await response.json(); + + return { clientSecret, error }; + }; + + const handlePayPress = async () => { + const { clientSecret, error: clientSecretError } = + await fetchPaymentIntentClientSecret(); + + if (clientSecretError) { + Alert.alert(`Error`, clientSecretError); + return; + } + + const shippingDetails: PaymentMethod.ShippingDetails = { + address: { + city: 'Houston', + country: 'US', + line1: '1459 Circle Drive', + postalCode: '77063', + state: 'Texas', + }, + name: 'John Doe', + }; + + console.log('hi'); + const { error, paymentIntent } = await confirmPayment(clientSecret, { + paymentMethodType: 'Affirm', + paymentMethodData: { + shippingDetails, + }, + }); + + if (error) { + Alert.alert(`Error code: ${error.code}`, error.message); + console.log('Payment confirmation error', error.message); + } else if (paymentIntent) { + Alert.alert( + 'Success', + `The payment was confirmed successfully! currency: ${paymentIntent.currency}` + ); + } + }; + + const handleCreatePaymentMethodPress = async () => { + const { paymentMethod, error } = await createPaymentMethod({ + paymentMethodType: 'Affirm', + }); + + if (error) { + Alert.alert(`Error code: ${error.code}`, error.message); + return; + } else { + Alert.alert('Success', `Payment method id: ${paymentMethod?.id}`); + } + }; + + return ( + + setEmail(value.nativeEvent.text)} + style={styles.input} + /> +