最近在项目中原生化支付模块,由于项目是面向美国加拿大用户的,所以需要集成Paypal支付和信用卡支付。本人菜鸟一个,第一次做项目,就被安排完成支付模块,我也是很无语啊,没办法,只能硬着头皮上啊。经过阅读文档,不断测试,最终成功了!!!(鼓掌)
今天主要讲一下信用卡和Paypal支付两部分。
信用卡需要自己定义UI,所以输入信用卡信息都是自己处理。信用卡信息的获取分为手动填写和扫描获取。
信用卡账号输入处理:
/**
* 信用卡号每隔四位加入一个空格
*/
private TextWatcher mCreditCardNumWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
modifyCardIcon(s.toString().replace(" ", ""));
if (TextUtils.isEmpty(s)) {
return;
}
//判断是否是在中间输入,需要重新计算
boolean isMiddle = (start + count) < (s.length());
//在末尾输入时,是否需要加入空格
boolean isNeedSpace = false;
if (!isMiddle && s.length() > 0 && s.length() % 5 == 0) {
isNeedSpace = true;
}
if (isMiddle || isNeedSpace || count > 1) {
String newStr = s.toString();
newStr = newStr.replace(" ", "");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < newStr.length(); i += 4) {
if (i > 0) {
sb.append(" ");
}
if (i + 4 <= newStr.length()) {
sb.append(newStr.substring(i, i + 4));
} else {
sb.append(newStr.substring(i, newStr.length()));
}
}
mCreditCardNum.removeOnTextChangedListener(this);
mCreditCardNum.setText(sb);
//如果是在末尾的话,或者加入的字符个数大于零的话(输入或者粘贴)
if (!isMiddle || count > 1) {
mCreditCardNum.setSelection(sb.length());
} else if (isMiddle) {
//如果是删除
if (count == 0) {
//如果删除时,光标停留在空格的前面,光标则要往前移一位
if ((start - before + 1) % 5 == 0) {
mCreditCardNum.setSelection((start - before) > 0 ? start - before : 0);
} else {
mCreditCardNum.setSelection((start - before + 1) > sb.length() ? sb.length() : (start - before + 1));
}
}
//如果是增加
else {
if ((start - before + count) % 5 == 0) {
mCreditCardNum.setSelection((start + count - before + 1) < sb.length() ? (start + count - before + 1) : sb.length());
} else {
mCreditCardNum.setSelection(start + count - before);
}
}
}
mCreditCardNum.addOnTextChangedListener(this);
}
}
@Override
public void afterTextChanged(Editable s) {
}
};
信用卡有效期输入处理:
/**
* 有效期的格式为MM/YY
*/
private TextWatcher mCreditCardExpirationDateWatcher = new TextWatcher() {
String beforeTC;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
beforeTC = s.toString();
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String currentInputDesc = "";
//插入
if (count != 0) {
String temp = s.toString();
if (temp.length() >= 1) {
currentInputDesc = temp.charAt(temp.length() - 1) + "";
}
if (ZERO.equals(beforeTC)) {
if (ZERO.equals(currentInputDesc)) {
// 第一位是零,输入的数字也是零,则设置为0
mCreditCardExpirationDate.setText(ZERO);
mCreditCardExpirationDate.setSelection(ZERO.length());
} else {
// 第一位是零,输入的数字不是零,补全斜杠
String aTemp = s.toString() + BACK_SLASH;
mCreditCardExpirationDate.setText(aTemp);
mCreditCardExpirationDate.setSelection(aTemp.length());
}
}
if (ONE.equals(beforeTC)) {
// 之前等于1
if (currentInputDesc.equals(ZERO)
|| currentInputDesc.equals(ONE)
|| currentInputDesc.equals(TWO)) {
// 并且输入的第二位数字满足 0,1,2月份要求
String aTemp = ONE + currentInputDesc + BACK_SLASH;
mCreditCardExpirationDate.setText(aTemp);
mCreditCardExpirationDate.setSelection(aTemp.length());
} else {
mCreditCardExpirationDate.setText(ONE);
mCreditCardExpirationDate.setSelection(ONE.length());
}
}
if (TextUtils.isEmpty(beforeTC)) {
temp = temp.replaceAll("/", "");
if (temp.length() == 1) {
//输入
if (!currentInputDesc.equals(ZERO)
&& !currentInputDesc.equals(ONE)) {
// 直接输入数字
String aTemp = ZERO + s.toString() + BACK_SLASH;
mCreditCardExpirationDate.setText(aTemp);
mCreditCardExpirationDate.setSelection(aTemp.length());
}
} else if (temp.length() > 1) {
String firstChar = temp.charAt(0) + "";
if (!firstChar.equals(ZERO) && !firstChar.equals(ONE)) {
temp = ZERO + temp;
}
temp = temp.substring(0, 2) + "/" + temp.substring(2, temp.length());
mCreditCardExpirationDate.setText(temp);
if (temp.length() > 5) {
mCreditCardExpirationDate.setSelection(5);
} else {
mCreditCardExpirationDate.setSelection(temp.length());
}
}
}
} else { //删除
int selectionStart = mCreditCardExpirationDate.getSelectionStart();
int textLength = mCreditCardExpirationDate.getText().length();
// 判断光标有没有移动,移动的话不删除文字,
if (selectionStart != textLength) {
mCreditCardExpirationDate.setText(beforeTC);
mCreditCardExpirationDate
.setSelection(mCreditCardExpirationDate.getText().length());
} else {
// 如果在最后,则删除
mCreditCardExpirationDate.setSelection(s.toString().length());
if (s.length() == 2) {
// 当只剩下三个字符的时候一下删除两个字符
int index = mCreditCardExpirationDate.getSelectionStart();
Editable editable = mCreditCardExpirationDate.getText();
editable.delete(index - 1, index);
}
}
}
}
@Override
public void afterTextChanged(Editable s) {
}
};
使用card.io:https://github.com/card-io/card.io-Android-SDK
SDK要求:
Android SDK版本16(Android 4.1)或更高版本。
集成过程:
在你的build.gradle:
compile 'io.card:android-sdk:5.5.1'
首先,我们假设你要从一个按钮启动扫描器,并且已经onClick在布局XML中通过设置按钮的处理器android:onClick=”onScanPress”。然后,添加方法如下:
public void onScanPress(View v) {
Intent scanIntent = new Intent(this, CardIOActivity.class);
// customize these values to suit your needs.
scanIntent.putExtra(CardIOActivity.EXTRA_REQUIRE_EXPIRY, true); // default: false
scanIntent.putExtra(CardIOActivity.EXTRA_REQUIRE_CVV, false); // default: false
scanIntent.putExtra(CardIOActivity.EXTRA_REQUIRE_POSTAL_CODE, false); // default: false
// MY_SCAN_REQUEST_CODE is arbitrary and is only used within this activity.
startActivityForResult(scanIntent, MY_SCAN_REQUEST_CODE);
}
接下来,我们将覆盖onActivityResult()获取扫描结果。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MY_SCAN_REQUEST_CODE) {
String resultDisplayStr;
if (data != null && data.hasExtra(CardIOActivity.EXTRA_SCAN_RESULT)) {
CreditCard scanResult = data.getParcelableExtra(CardIOActivity.EXTRA_SCAN_RESULT);
// Never log a raw card number. Avoid displaying it, but if necessary use getFormattedCardNumber()
resultDisplayStr = "Card Number: " + scanResult.getRedactedCardNumber() + "\n";
// Do something with the raw number, e.g.:
// myService.setCardNumber( scanResult.cardNumber );
if (scanResult.isExpiryValid()) {
resultDisplayStr += "Expiration Date: " + scanResult.expiryMonth + "/" + scanResult.expiryYear + "\n";
}
if (scanResult.cvv != null) {
// Never log or display a CVV
resultDisplayStr += "CVV has " + scanResult.cvv.length() + " digits.\n";
}
if (scanResult.postalCode != null) {
resultDisplayStr += "Postal Code: " + scanResult.postalCode + "\n";
}
}
else {
resultDisplayStr = "Scan was canceled.";
}
// do something with resultDisplayStr, maybe display it in a textView
// resultTextView.setText(resultDisplayStr);
}
// else handle other activity results
}
信用卡的整个过程还是比较简单的,就不做过多的描述。
今天的重点来了,Paypal支付,废话不多说,直接进入主题。
我们项目中使用的是Braintree支付,这里要来讲一下Braintree和Paypal的区别与联系:
支付网关(Payment Gateway)是在商户的在线商城网站和商户的银行收款账户之间,搭建一个加密的支付信息通道,以便安全地将消费者通过浏览器在网站上购买时所输入的账户信息(如信用卡、姓名等)安全地传输到银行端,并将付款行的授权返回给收款行。Braintree就是支付网关。
支付处理系统(Processor)是链接消费者账户银行和商户收款银行之间的交易系统,确保交易资金可以顺利地从消费者付款行账户进入到商户的收款行账户。Paypal就是支付管理系统。
具体的怎么操作就不用管了,这是他们后台支付的问题。不需要我们来处理。现在来看一下Braintree的支付流程:
重点来了,集成:
官方文档:https://developer.paypal.com/docs/accept-payments/express-checkout/ec-braintree-sdk/client-side/android/v2/)
SDK要求:Android API >= 16
在你的build.gradle,添加以下内容:
dependencies {
compile 'com.braintreepayments.api:braintree:2.+'
}
对于PayPal,必须将URL方案定义为接受返回浏览器开关。编辑你AndroidManifest.xml的包含BraintreeBrowserSwitchActivity并设置android:scheme:
<activity android:name="com.braintreepayments.api.BraintreeBrowserSwitchActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="${applicationId}.braintree" />
intent-filter>
activity>
设置BraintreeFragment只是一个调用BraintreeFragment.newInstance(activity, authorization)与当前Activity和客户端令牌。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
mBraintreeFragment = BraintreeFragment.newInstance(this, mAuthorization);
// mBraintreeFragment is ready to use!
} catch (InvalidArgumentException e) {
// There was an issue with your authorization string.
}
}
这个方法将会:
注册监听事件:
PaymentMethodNonceCreatedListener当a PaymentMethodNonce被成功标记时被调用。从这发送nonce PaymentMethodNonce到你的服务器来创建一个事务。
@Override
public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) {
// Send this nonce to your server
String nonce = paymentMethodNonce.getNonce();
}
BraintreeCancelListener在BraintreeFragment被通知Activity#RESULT_CANCELED结果代码时被调用Fragment#onActivityResult。
@Override
public void onCancel(int requestCode) {
// Use this to handle a canceled activity, if the given requestCode is important.
// You may want to use this callback to hide loading indicators, and prepare your UI for input
}
BraintreeErrorListener在出现错误时调用。ErrorWithResponse在请求发生验证错误时调用。Exception在发生网络问题或服务器错误等错误时引发。
@Override
public void onError(Exception error) {
if (error instanceof ErrorWithResponse) {
ErrorWithResponse errorWithResponse = (ErrorWithResponse) error;
BraintreeError cardErrors = errorWithResponse.errorFor("creditCard");
if (cardErrors != null) {
// There is an issue with the credit card.
BraintreeError expirationMonthError = cardErrors.errorFor("expirationMonth");
if (expirationMonthError != null) {
// There is an issue with the expiration month.
setErrorMessage(expirationMonthError.getMessage());
}
}
}
}
您的服务器负责生成客户端令牌,其中包含您的客户端初始化客户端SDK所需的授权和配置详细信息。
从您的服务器请求客户端令牌,然后初始化Braintree并提供下拉式UI(本示例使用Android异步Http客户端从服务器请求客户端令牌 - 适应您自己的设置):
AsyncHttpClient client = new AsyncHttpClient();
client.get("https://your-server/client_token", new TextHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, String clientToken) {
this.clientToken = clientToken;
}
});
一次性结账是使用PayPal创建单个交易的简单方法。首先,确保你已经定义了你的URL方案。然后,PayPal.requestOneTimePayment用来开始这个过程。一个示例集成可能如下所示:
public void setupBraintreeAndStartExpressCheckout() {
PayPalRequest request = new PayPalRequest("1")
.currencyCode("USD")
.intent(PayPalRequest.INTENT_AUTHORIZE);
PayPal.requestOneTimePayment(mBraintreeFragment, request);
}
支付事件的监听:
yourButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
setupBraintreeAndStartExpressCheckout();
}
});
当您收到回拨时onPaymentMethodNonceCreated,您可以查询PayPalAccountNonce您返回的对象以获取特定的客户信息。
@Override
public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) {
// Send nonce to server
String nonce = paymentMethodNonce.getNonce();
}
将生成的付款方式随机数发送到您的服务器:
void postNonceToServer(String nonce) {
AsyncHttpClient client = new AsyncHttpClient();
RequestParams params = new RequestParams();
params.put("payment_method_nonce", nonce);
client.post("http://your-server/checkout", params,
new AsyncHttpResponseHandler() {
// Your implementation here
}
);
}
好吧,整个流程完了。开始的时候不了解,踩了很多坑。国内也没有完整的教程,所以就自己做的过程整理了一下。第一次写,写的不好,请大家多多包涵。