Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for Bacs debit payments #1352

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class PaymentMethodCreateParamsFactory(
PaymentMethod.Type.PayPal -> createPayPalParams()
PaymentMethod.Type.Affirm -> createAffirmParams()
PaymentMethod.Type.CashAppPay -> createCashAppParams()
PaymentMethod.Type.BacsDebit -> createBacsParams()
else -> {
throw Exception("This paymentMethodType is not supported yet")
}
Expand Down Expand Up @@ -97,6 +98,28 @@ class PaymentMethodCreateParamsFactory(
throw PaymentMethodCreateParamsException("You must provide billing details")
}

@Throws(PaymentMethodCreateParamsException::class)
private fun createBacsParams(): PaymentMethodCreateParams {
billingDetailsParams?.let {
val accountNumber = getValOr(paymentMethodData, "accountNumber", null) ?: run {
throw PaymentMethodCreateParamsException("You must provide Account number")
}
val sortCode = getValOr(paymentMethodData, "sortCode", null) ?: run {
throw PaymentMethodCreateParamsException("You must provide Sort code")
}

return PaymentMethodCreateParams.create(
bacsDebit = PaymentMethodCreateParams.BacsDebit(
accountNumber = accountNumber,
sortCode = sortCode
),
billingDetails = it
)
}

throw PaymentMethodCreateParamsException("You must provide billing details")
}

@Throws(PaymentMethodCreateParamsException::class)
private fun createOXXOParams(): PaymentMethodCreateParams {
billingDetailsParams?.let {
Expand Down Expand Up @@ -230,6 +253,7 @@ class PaymentMethodCreateParamsFactory(
PaymentMethod.Type.AuBecsDebit,
PaymentMethod.Type.Klarna,
PaymentMethod.Type.PayPal,
PaymentMethod.Type.BacsDebit,
PaymentMethod.Type.CashAppPay -> {
val params = createPaymentMethodParams(paymentMethodType)

Expand Down
22 changes: 22 additions & 0 deletions e2e-tests/bacs-payment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
appId: ${APP_ID}
---
- launchApp
- tapOn: 'Bank Debits'
- tapOn: 'Bacs Direct Debit payment'
- assertVisible:
text: 'E-mail'
- tapOn:
text: 'E-mail'
- inputText: 'test@stripe.com'
- tapOn:
text: 'sortCode'
- inputText: '108800'
- tapOn:
text: 'accountNumber'
- inputText: '90012345'
- tapOn:
text: 'Pay'
retryTapIfNoChange: false
- assertVisible:
text: 'Processing'
- tapOn: 'OK'
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -650,4 +650,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: d98ea981c14c48ddbbd4f2d6bcf130927b15a93f

COCOAPODS: 1.11.3
COCOAPODS: 1.12.0
1 change: 1 addition & 0 deletions example/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function getKeys(payment_method?: string) {
secret_key = process.env.STRIPE_SECRET_KEY_WECHAT;
break;
case 'paypal':
case 'bacs_debit':
publishable_key = process.env.STRIPE_PUBLISHABLE_KEY_UK;
secret_key = process.env.STRIPE_SECRET_KEY_UK;
break;
Expand Down
6 changes: 6 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import PayPalScreen from './screens/PayPalScreen';
import AffirmScreen from './screens/AffirmScreen';
import CollectBankAccountScreen from './screens/CollectBankAccountScreen';
import CashAppScreen from './screens/CashAppScreen';
import BacsDebitPaymentScreen from './screens/BacsDebitPaymentScreen';

const Stack = createNativeStackNavigator<RootStackParamList>();

Expand Down Expand Up @@ -83,6 +84,7 @@ export type RootStackParamList = {
CashAppScreen: undefined;
AffirmScreen: undefined;
CollectBankAccountScreen: undefined;
BacsDebitPaymentScreen: undefined;
};

declare global {
Expand Down Expand Up @@ -200,6 +202,10 @@ export default function App() {
name="SepaPaymentScreen"
component={SepaPaymentScreen}
/>
<Stack.Screen
name="BacsDebitPaymentScreen"
component={BacsDebitPaymentScreen}
/>
<Stack.Screen
name="SepaSetupFuturePaymentScreen"
component={SepaSetupFuturePaymentScreen}
Expand Down
136 changes: 136 additions & 0 deletions example/src/screens/BacsDebitPaymentScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { PaymentIntent, BillingDetails } from '@stripe/stripe-react-native';
import React, { useState } from 'react';
import { Alert, StyleSheet, TextInput } from 'react-native';
import { useConfirmPayment } 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 BacsPaymentScreen() {
const [email, setEmail] = useState('');
const { confirmPayment, loading } = useConfirmPayment();
const [sortCode, setSortCode] = useState('');
const [accountNumber, setAccountNumber] = useState('');
const [canPay, setCanPay] = useState(true);

const fetchPaymentIntentClientSecret = async () => {
const response = await fetch(`${API_URL}/create-payment-intent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
currency: 'gbp',
items: ['id-1'],
payment_method_types: ['bacs_debit'],
}),
});
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 billingDetails: BillingDetails = {
name: 'John Doe',
email: email,
address: {
country: 'UK',
line1: 'test',
city: 'test',
},
};
setCanPay(false);

const { error, paymentIntent } = await confirmPayment(clientSecret, {
paymentMethodType: 'BacsDebit',
paymentMethodData: { billingDetails, sortCode, accountNumber },
});

if (error) {
Alert.alert(`Error code: ${error.code}`, error.message);
} else if (paymentIntent) {
if (paymentIntent.status === PaymentIntent.Status.Processing) {
Alert.alert(
'Processing',
`The debit has been successfully submitted and is now processing.`
);
} else if (paymentIntent.status === PaymentIntent.Status.Succeeded) {
Alert.alert(
'Success',
`The payment was confirmed successfully! currency: ${paymentIntent.currency}`
);
} else {
Alert.alert('Payment status:', paymentIntent.status);
}
}
setCanPay(true);
};

return (
<PaymentScreen paymentMethod="bacs_debit">
<TextInput
autoCapitalize="none"
placeholder="E-mail"
keyboardType="email-address"
onChange={(value) => setEmail(value.nativeEvent.text)}
style={styles.input}
/>
<TextInput
autoCapitalize="characters"
placeholder="sortCode"
onChange={(value) => setSortCode(value.nativeEvent.text.toLowerCase())}
style={styles.input}
/>

<TextInput
autoCapitalize="characters"
placeholder="accountNumber"
onChange={(value) =>
setAccountNumber(value.nativeEvent.text.toLowerCase())
}
style={styles.input}
/>
<Button
variant="primary"
onPress={handlePayPress}
title="Pay"
disabled={!canPay}
accessibilityLabel="Pay"
loading={loading}
/>
</PaymentScreen>
);
}

const styles = StyleSheet.create({
cardField: {
width: '100%',
height: 50,
marginVertical: 30,
},
row: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
},
text: {
marginLeft: 12,
},
input: {
height: 44,
borderBottomColor: colors.slate,
borderBottomWidth: 1.5,
marginBottom: 20,
},
});
8 changes: 8 additions & 0 deletions example/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ export default function HomeScreen() {
}}
/>
</View>
<View style={styles.buttonContainer}>
<Button
title="Bacs Direct Debit payment"
onPress={() => {
navigation.navigate('BacsDebitPaymentScreen');
}}
/>
</View>
<View style={styles.buttonContainer}>
<Button
title="BECS Direct Debit payment"
Expand Down
4 changes: 4 additions & 0 deletions example/src/screens/PaymentsUICompleteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export default function PaymentsUICompleteScreen() {
},
});
const { paymentIntent, ephemeralKey, customer } = await response.json();
console.log({
paymentIntent,
API_URL,
});
setClientSecret(paymentIntent);
return {
paymentIntent,
Expand Down
29 changes: 29 additions & 0 deletions ios/PaymentMethodFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class PaymentMethodFactory {
return try createAffirmPaymentMethodParams()
case STPPaymentMethodType.cashApp:
return try createCashAppPaymentMethodParams()
case STPPaymentMethodType.bacsDebit:
return try createBacsPaymentMethodParams()
// case STPPaymentMethodType.weChatPay:
// return try createWeChatPayPaymentMethodParams()
default:
Expand Down Expand Up @@ -110,6 +112,8 @@ class PaymentMethodFactory {
return nil
case STPPaymentMethodType.cashApp:
return nil
case STPPaymentMethodType.bacsDebit:
return nil
default:
throw PaymentMethodError.paymentNotSupported
}
Expand Down Expand Up @@ -278,6 +282,28 @@ class PaymentMethodFactory {

return STPPaymentMethodParams(sepaDebit: params, billingDetails: billingDetails, metadata: nil)
}

private func createBacsPaymentMethodParams() throws -> STPPaymentMethodParams {
let params = STPPaymentMethodBacsDebitParams()

guard let billingDetails = billingDetailsParams else {
throw PaymentMethodError.bacsPaymentMissingParams
}

guard let accountNumber = self.paymentMethodData?["accountNumber"] as? String else {
throw PaymentMethodError.bacsPaymentMissingParams
}

guard let sortCode = self.paymentMethodData?["sortCode"] as? String else {
throw PaymentMethodError.bacsPaymentMissingParams
}

params.accountNumber = accountNumber
params.sortCode = sortCode

return STPPaymentMethodParams(bacsDebit: params, billingDetails: billingDetails, metadata: nil)
}


private func createOXXOPaymentMethodParams() throws -> STPPaymentMethodParams {
let params = STPPaymentMethodOXXOParams()
Expand Down Expand Up @@ -404,6 +430,7 @@ enum PaymentMethodError: Error {
case cardPaymentOptionsMissingParams
case bancontactPaymentMissingParams
case sepaPaymentMissingParams
case bacsPaymentMissingParams
case giropayPaymentMissingParams
case p24PaymentMissingParams
case afterpayClearpayPaymentMissingParams
Expand Down Expand Up @@ -431,6 +458,8 @@ extension PaymentMethodError: LocalizedError {
return NSLocalizedString("You must provide billing details", comment: "Create payment error")
case .sepaPaymentMissingParams:
return NSLocalizedString("You must provide billing details and IBAN", comment: "Create payment error")
case .bacsPaymentMissingParams:
return NSLocalizedString("You must provide billing details, Sort code and Account number", comment: "Create payment error")
case .epsPaymentMissingParams:
return NSLocalizedString("You must provide billing details", comment: "Create payment error")
case .afterpayClearpayPaymentMissingParams:
Expand Down
12 changes: 11 additions & 1 deletion src/types/PaymentIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export type ConfirmParams =
| USBankAccountParams
| PayPalParams
| AffirmParams
| CashAppParams;
| CashAppParams
| BacsParams;

export type ConfirmOptions = PaymentMethod.ConfirmOptions;

Expand Down Expand Up @@ -166,6 +167,15 @@ export interface SepaParams {
};
}

export interface BacsParams {
paymentMethodType: 'BacsDebit';
paymentMethodData: {
accountNumber: string;
sortCode: string;
Comment on lines +176 to +177
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that, although not required, it would be best for us to encrypt this information before transferring it over the Bridge

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charliecruzan-stripe Could you please provide advice on what is the best way to do this? Also, does the new architecture remove the necessity for this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new architecture would remove the necessity for this, so we should wait for that migration before working on this feature

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charliecruzan-stripe Any idea the type of timeframes for this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not at the moment, the new architecture is still very experimental

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charliecruzan-stripe Is there anything I can do to help get this over the line? If the new architecture isn't ready I'm happy to try and add encryption to this PR if that helps.

billingDetails: BillingDetails;
};
}

export interface GiropayParams {
paymentMethodType: 'Giropay';
paymentMethodData: {
Expand Down
15 changes: 13 additions & 2 deletions src/types/PaymentMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export type CreateParams =
| USBankAccountParams
| PayPalParams
| AffirmParams
| CashAppParams;
| CashAppParams
| BacsParams;

export type ConfirmParams = CreateParams;

Expand Down Expand Up @@ -126,6 +127,15 @@ export interface SepaParams {
};
}

export interface BacsParams {
paymentMethodType: 'BacsDebit';
paymentMethodData: {
accountNumber: string;
sortCode: string;
billingDetails: BillingDetails;
};
}

export interface GiropayParams {
paymentMethodType: 'Giropay';
paymentMethodData: {
Expand Down Expand Up @@ -303,7 +313,8 @@ export type Type =
| 'Upi'
| 'USBankAccount'
| 'PayPal'
| 'Unknown';
| 'Unknown'
| 'BacsDebit';

export type CollectBankAccountParams = {
paymentMethodType: 'USBankAccount';
Expand Down