diff --git a/packages/react-native-payments/android/build.gradle b/packages/react-native-payments/android/build.gradle index 2dac59c2..75b387bc 100644 --- a/packages/react-native-payments/android/build.gradle +++ b/packages/react-native-payments/android/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion 28 + buildToolsVersion "28.0.3" defaultConfig { minSdkVersion 16 - targetSdkVersion 22 + targetSdkVersion 28 versionCode 1 versionName "1.0" ndk { @@ -19,7 +19,7 @@ android { } dependencies { - compile 'com.facebook.react:react-native:+' - compile 'com.google.android.gms:play-services-wallet:11.0.4' - compile 'com.android.support:support-v4:23.0.1' -} + implementation 'com.facebook.react:react-native:+' + implementation 'com.google.android.gms:play-services-wallet:17.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' +} \ No newline at end of file diff --git a/packages/react-native-payments/android/src/main/AndroidManifest.xml b/packages/react-native-payments/android/src/main/AndroidManifest.xml index 3bc7e98e..0a14dbfd 100644 --- a/packages/react-native-payments/android/src/main/AndroidManifest.xml +++ b/packages/react-native-payments/android/src/main/AndroidManifest.xml @@ -2,4 +2,9 @@ + + + diff --git a/packages/react-native-payments/android/src/main/java/com/reactnativepayments/ReactNativePaymentsModule.java b/packages/react-native-payments/android/src/main/java/com/reactnativepayments/ReactNativePaymentsModule.java index a47b50d4..7ff9fff3 100644 --- a/packages/react-native-payments/android/src/main/java/com/reactnativepayments/ReactNativePaymentsModule.java +++ b/packages/react-native-payments/android/src/main/java/com/reactnativepayments/ReactNativePaymentsModule.java @@ -5,11 +5,11 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.NonNull; import android.app.Fragment; import android.app.FragmentManager; -import android.support.annotation.RequiresPermission; +import androidx.annotation.RequiresPermission; import android.util.Log; import com.facebook.react.bridge.Callback; @@ -22,6 +22,10 @@ import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.identity.intents.model.UserAddress; import com.google.android.gms.wallet.*; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.BaseActivityEventListener; @@ -38,19 +42,19 @@ import java.util.List; import java.util.Map; -public class ReactNativePaymentsModule extends ReactContextBaseJavaModule implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { +import org.json.JSONException; +import org.json.JSONObject; + +public class ReactNativePaymentsModule extends ReactContextBaseJavaModule { private static final int LOAD_MASKED_WALLET_REQUEST_CODE = 88; - private static final int LOAD_FULL_WALLET_REQUEST_CODE = 89; - // Google API Client - private GoogleApiClient mGoogleApiClient = null; + // Payments Client + private PaymentsClient mPaymentsClient; // Callbacks private static Callback mShowSuccessCallback = null; private static Callback mShowErrorCallback = null; - private static Callback mGetFullWalletSuccessCallback= null; - private static Callback mGetFullWalletErrorCallback = null; public static final String REACT_CLASS = "ReactNativePayments"; @@ -69,13 +73,12 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, switch (resultCode) { case Activity.RESULT_OK: if (data != null) { - MaskedWallet maskedWallet = - data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET); - Log.i(REACT_CLASS, "ANDROID PAY SUCCESS" + maskedWallet.getEmail()); - Log.i(REACT_CLASS, "ANDROID PAY SUCCESS" + buildAddressFromUserAddress(maskedWallet.getBuyerBillingAddress())); + PaymentData paymentData = PaymentData.getFromIntent(data); + + Log.i(REACT_CLASS, "ANDROID PAY SUCCESS" + buildAddressFromUserAddress(paymentData.getCardInfo().getBillingAddress())); - UserAddress userAddress = maskedWallet.getBuyerShippingAddress(); + UserAddress userAddress = paymentData.getShippingAddress(); WritableNativeMap shippingAddress = userAddress != null ? buildAddressFromUserAddress(userAddress) : null; @@ -83,12 +86,33 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, // TODO: Move into function WritableNativeMap paymentDetails = new WritableNativeMap(); - paymentDetails.putString("paymentDescription", maskedWallet.getPaymentDescriptions()[0]); - paymentDetails.putString("payerEmail", maskedWallet.getEmail()); + paymentDetails.putString("payerEmail", paymentData.getEmail()); paymentDetails.putMap("shippingAddress", shippingAddress); - paymentDetails.putString("googleTransactionId", maskedWallet.getGoogleTransactionId()); + paymentDetails.putString("googleTransactionId", paymentData.getGoogleTransactionId()); + + WritableNativeMap cardInfo = buildCardInfo(paymentData.getCardInfo()); + paymentDetails.putMap("cardInfo", cardInfo); + + String serializedPaymentToken = paymentData.getPaymentMethodToken().getToken(); + try { + JSONObject paymentTokenJson = new JSONObject(serializedPaymentToken); + String protocolVersion = paymentTokenJson.getString("protocolVersion"); + String signature = paymentTokenJson.getString("signature"); + String signedMessage = paymentTokenJson.getString("signedMessage"); + + WritableNativeMap paymentToken = new WritableNativeMap(); + paymentToken.putString("protocolVersion", protocolVersion); + paymentToken.putString("signature", signature); + paymentToken.putString("signedMessage", signedMessage); - sendEvent(reactContext, "NativePayments:onuseraccept", paymentDetails); + paymentDetails.putMap("paymentToken", paymentToken); + + sendEvent(reactContext, "NativePayments:onuseraccept", paymentDetails); + + } catch (JSONException e) { + Log.e(REACT_CLASS, "ANDROID PAY JSON ERROR", e); + mShowErrorCallback.invoke(errorCode); + } } break; case Activity.RESULT_CANCELED: @@ -102,17 +126,6 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, break; } break; - case LOAD_FULL_WALLET_REQUEST_CODE: - if (resultCode == Activity.RESULT_OK && data != null) { - FullWallet fullWallet = data.getParcelableExtra(WalletConstants.EXTRA_FULL_WALLET); - String tokenJSON = fullWallet.getPaymentMethodToken().getToken(); - Log.i(REACT_CLASS, "FULL WALLET SUCCESS" + tokenJSON); - - mGetFullWalletSuccessCallback.invoke(tokenJSON); - } else { - Log.i(REACT_CLASS, "FULL WALLET FAILURE"); - mGetFullWalletErrorCallback.invoke(); - } case WalletConstants.RESULT_ERROR:activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); // handleError(errorCode); break; @@ -151,25 +164,42 @@ public void getSupportedGateways(Callback errorCallback, Callback successCallbac } @ReactMethod - public void canMakePayments(ReadableMap paymentMethodData, Callback errorCallback, Callback successCallback) { - final Callback callback = successCallback; - IsReadyToPayRequest req = IsReadyToPayRequest.newBuilder() - .addAllowedCardNetwork(WalletConstants.CardNetwork.MASTERCARD) - .addAllowedCardNetwork(WalletConstants.CardNetwork.VISA) - .build(); + public void canMakePayments(ReadableMap paymentMethodData, final Callback errorCallback, final Callback successCallback) { + IsReadyToPayRequest.Builder builder = IsReadyToPayRequest.newBuilder(); + + ReadableArray allowedCardNetworks = paymentMethodData.getArray("supportedNetworks"); + if (allowedCardNetworks != null) { + builder.addAllowedCardNetworks(buildAllowedCardNetworks(allowedCardNetworks)); + } + + ReadableArray allowedPaymentMethods = paymentMethodData.getArray("allowedPaymentMethods"); + if (allowedPaymentMethods != null) { + builder.addAllowedPaymentMethods(buildAllowedPaymentMethods(allowedPaymentMethods)); + } + + IsReadyToPayRequest request = builder.build(); + int environment = getEnvironmentFromPaymentMethodData(paymentMethodData); - if (mGoogleApiClient == null) { - buildGoogleApiClient(getCurrentActivity(), environment); + if (mPaymentsClient == null) { + buildPaymentsClient(getCurrentActivity(), environment); } - Wallet.Payments.isReadyToPay(mGoogleApiClient, req) - .setResultCallback(new ResultCallback() { - @Override - public void onResult(@NonNull BooleanResult booleanResult) { - callback.invoke(booleanResult.getValue()); + Task task = mPaymentsClient.isReadyToPay(request); + task.addOnCompleteListener( + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + boolean result = task.getResult(ApiException.class); + if (result) { + successCallback.invoke(result); + } + } catch (ApiException e) { + errorCallback.invoke(e.getMessage()); } - }); + } + }); } @ReactMethod @@ -198,53 +228,40 @@ public void show( final PaymentMethodTokenizationParameters parameters = buildTokenizationParametersFromPaymentMethodData(paymentMethodData); - // TODO: clean up MaskedWalletRequest ReadableMap total = details.getMap("total").getMap("amount"); - final MaskedWalletRequest maskedWalletRequest = MaskedWalletRequest.newBuilder() - .setPaymentMethodTokenizationParameters(parameters) - .setPhoneNumberRequired(shouldRequestPayerPhone) - .setShippingAddressRequired(shouldRequestShipping) - .setEstimatedTotalPrice(total.getString("value")) - .setCurrencyCode(total.getString("currency")) - .build(); - int environment = getEnvironmentFromPaymentMethodData(paymentMethodData); - if (mGoogleApiClient == null) { - buildGoogleApiClient(getCurrentActivity(), environment); - } - Wallet.Payments.loadMaskedWallet(mGoogleApiClient, maskedWalletRequest, LOAD_MASKED_WALLET_REQUEST_CODE); - } + PaymentDataRequest.Builder builder = PaymentDataRequest.newBuilder() + .setTransactionInfo(TransactionInfo.newBuilder() + .setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL) + .setTotalPrice(total.getString("value")) + .setCurrencyCode(total.getString("currency")) + .build()) + .setPhoneNumberRequired(shouldRequestPayerPhone) + .setShippingAddressRequired(shouldRequestShipping) + .setPaymentMethodTokenizationParameters(parameters); - @ReactMethod - public void getFullWalletAndroid( - String googleTransactionId, - ReadableMap paymentMethodData, - ReadableMap details, - Callback errorCallback, - Callback successCallback - ) { - mGetFullWalletSuccessCallback = successCallback; - mGetFullWalletErrorCallback = errorCallback; - ReadableMap total = details.getMap("total").getMap("amount"); - Log.i(REACT_CLASS, "ANDROID PAY getFullWalletAndroid" + details.getMap("total").getMap("amount")); + ReadableArray allowedCardNetworks = paymentMethodData.getArray("supportedNetworks"); + if (allowedCardNetworks != null) { + builder.setCardRequirements(CardRequirements.newBuilder() + .addAllowedCardNetworks(buildAllowedCardNetworks(allowedCardNetworks)).build() + ); + } + + ReadableArray allowedPaymentMethods = paymentMethodData.getArray("allowedPaymentMethods"); + if (allowedPaymentMethods != null) { + builder.addAllowedPaymentMethods(buildAllowedPaymentMethods(allowedPaymentMethods)); + } - FullWalletRequest fullWalletRequest = FullWalletRequest.newBuilder() - .setGoogleTransactionId(googleTransactionId) - .setCart(Cart.newBuilder() - .setCurrencyCode(total.getString("currency")) - .setTotalPrice(total.getString("value")) - .setLineItems(buildLineItems(details.getArray("displayItems"))) - .build()) - .build(); + PaymentDataRequest request = builder.build(); int environment = getEnvironmentFromPaymentMethodData(paymentMethodData); - if (mGoogleApiClient == null) { - buildGoogleApiClient(getCurrentActivity(), environment); - } - Wallet.Payments.loadFullWallet(mGoogleApiClient, fullWalletRequest, LOAD_FULL_WALLET_REQUEST_CODE); + if (mPaymentsClient == null) buildPaymentsClient(getCurrentActivity(), environment); + + AutoResolveHelper.resolveTask( + mPaymentsClient.loadPaymentData(request), getCurrentActivity(), LOAD_MASKED_WALLET_REQUEST_CODE); } // Private Method @@ -280,6 +297,64 @@ private static PaymentMethodTokenizationParameters buildTokenizationParametersFr } } + protected static List buildAllowedPaymentMethods(ReadableArray allowedPaymentMethods) { + List result = new ArrayList(); + int size = allowedPaymentMethods.size(); + for (int i = 0; i < size; ++i) { + int allowedPaymentMethod = allowedPaymentMethods.getInt(i); + result.add(allowedPaymentMethod); + } + + return result; + } + + protected static List buildAllowedCardNetworks(ReadableArray allowedCardNetworks) { + List result = new ArrayList(); + int size = allowedCardNetworks.size(); + for (int i = 0; i < size; ++i) { + String allowedCardNetwork = allowedCardNetworks.getString(i); + switch (allowedCardNetwork) { + case "visa": + result.add(WalletConstants.CARD_NETWORK_VISA); + break; + case "mastercard": + result.add(WalletConstants.CARD_NETWORK_MASTERCARD); + break; + case "amex": + result.add(WalletConstants.CARD_NETWORK_AMEX); + break; + case "discover": + result.add(WalletConstants.CARD_NETWORK_DISCOVER); + break; + case "interac": + result.add(WalletConstants.CARD_NETWORK_INTERAC); + break; + case "jcb": + result.add(WalletConstants.CARD_NETWORK_JCB); + break; + default: + result.add(WalletConstants.CARD_NETWORK_OTHER); + } + } + + return result; + } + + protected static WritableNativeMap buildCardInfo(CardInfo cardInfo) { + + if (cardInfo == null) return null; + + + WritableNativeMap result = new WritableNativeMap(); + + result.putInt("cardClass", cardInfo.getCardClass()); + result.putString("cardDescription", cardInfo.getCardDescription()); + result.putString("cardDetails", cardInfo.getCardDetails()); + result.putString("cardNetwork", cardInfo.getCardNetwork()); + + return result; + } + private static List buildLineItems(ReadableArray displayItems) { List list = new ArrayList(); @@ -305,6 +380,8 @@ private static List buildLineItems(ReadableArray displayItems) { private static WritableNativeMap buildAddressFromUserAddress(UserAddress userAddress) { WritableNativeMap address = new WritableNativeMap(); + if (userAddress == null) return address; + address.putString("recipient", userAddress.getName()); address.putString("organization", userAddress.getCompanyName()); address.putString("addressLine", userAddress.getAddress1()); @@ -336,36 +413,12 @@ private int getEnvironmentFromPaymentMethodData(ReadableMap paymentMethodData) { : WalletConstants.ENVIRONMENT_PRODUCTION; } - // Google API Client - // --------------------------------------------------------------------------------------------- - private void buildGoogleApiClient(Activity currentActivity, int environment) { - mGoogleApiClient = new GoogleApiClient.Builder(currentActivity) - .addConnectionCallbacks(this) - .addOnConnectionFailedListener(this) - .addApi(Wallet.API, new Wallet.WalletOptions.Builder() - .setEnvironment(environment) - .setTheme(WalletConstants.THEME_LIGHT) - .build()) - .build(); - mGoogleApiClient.connect(); - } - - @Override - public void onConnected(Bundle connectionHint) { -// mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); - } - - - @Override - public void onConnectionFailed(ConnectionResult result) { - // Refer to Google Play documentation for what errors can be logged - Log.i(REACT_CLASS, "Connection failed: ConnectionResult.getErrorCode() = " + result.getErrorCode()); - } - - @Override - public void onConnectionSuspended(int cause) { - // Attempts to reconnect if a disconnect occurs - Log.i(REACT_CLASS, "Connection suspended"); - mGoogleApiClient.connect(); + protected void buildPaymentsClient(Activity currentActivity, int environment) { + mPaymentsClient = Wallet.getPaymentsClient( + currentActivity, + new Wallet.WalletOptions.Builder() + .setEnvironment(environment) + .build() + ); } } diff --git a/packages/react-native-payments/android/src/main/res/values/strings.xml b/packages/react-native-payments/android/src/main/res/values/strings.xml new file mode 100644 index 00000000..439eefd2 --- /dev/null +++ b/packages/react-native-payments/android/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + react-native-payments + \ No newline at end of file diff --git a/packages/react-native-payments/lib/js/NativePayments.js b/packages/react-native-payments/lib/js/NativePayments.js index 2485d601..22ff4b40 100644 --- a/packages/react-native-payments/lib/js/NativePayments.js +++ b/packages/react-native-payments/lib/js/NativePayments.js @@ -1,6 +1,6 @@ // @flow -import type { PaymentDetailsBase, PaymentComplete } from './types'; +import type { CanMakePayments, PaymentDetailsBase, PaymentComplete } from './types'; import { NativeModules, Platform } from 'react-native'; const { ReactNativePayments } = NativeModules; @@ -8,26 +8,25 @@ const { ReactNativePayments } = NativeModules; const IS_ANDROID = Platform.OS === 'android'; const NativePayments: { - canMakePayments: boolean, + canMakePayments: CanMakePayments => Promise, supportedGateways: Array, createPaymentRequest: PaymentDetailsBase => Promise, handleDetailsUpdate: PaymentDetailsBase => Promise, show: () => Promise, abort: () => Promise, - complete: PaymentComplete => Promise, - getFullWalletAndroid: string => Promise + complete: PaymentComplete => Promise } = { supportedGateways: IS_ANDROID - ? ['stripe', 'braintree'] // On Android, Payment Gateways are supported out of the gate. + ? [] // On Android, Payment Gateways are supported out of the gate. : ReactNativePayments ? ReactNativePayments.supportedGateways : [], - canMakePayments(methodData: object) { + canMakePayments(methodData?: CanMakePayments) { return new Promise((resolve, reject) => { if (IS_ANDROID) { ReactNativePayments.canMakePayments( methodData, (err) => reject(err), - (canMakePayments) => resolve(true) + () => resolve(true) ); return; @@ -133,30 +132,6 @@ const NativePayments: { resolve(true); }); }); - }, - - getFullWalletAndroid(googleTransactionId: string, paymentMethodData: object, details: object): Promise { - return new Promise((resolve, reject) => { - if (!IS_ANDROID) { - reject(new Error('This method is only available on Android.')); - - return; - } - - ReactNativePayments.getFullWalletAndroid( - googleTransactionId, - paymentMethodData, - details, - (err) => reject(err), - (serializedPaymentToken) => resolve({ - serializedPaymentToken, - paymentToken: JSON.parse(serializedPaymentToken), - /** Leave previous typo in order not to create a breaking change **/ - serializedPaymenToken: serializedPaymentToken, - paymenToken: JSON.parse(serializedPaymentToken) - }) - ); - }); } }; diff --git a/packages/react-native-payments/lib/js/PaymentRequest.js b/packages/react-native-payments/lib/js/PaymentRequest.js index 44c4ad85..90f82829 100644 --- a/packages/react-native-payments/lib/js/PaymentRequest.js +++ b/packages/react-native-payments/lib/js/PaymentRequest.js @@ -211,7 +211,7 @@ export default class PaymentRequest { const normalizedDetails = convertDetailAmountsToString(details); // Validate gateway config if present - if (hasGatewayConfig(platformMethodData)) { + if (IS_IOS && hasGatewayConfig(platformMethodData)) { validateGateway( getGatewayName(platformMethodData), NativePayments.supportedGateways @@ -312,35 +312,33 @@ export default class PaymentRequest { } _getPlatformDetailsAndroid(details: { + cardInfo: Object, googleTransactionId: string, payerEmail: string, - paymentDescription: string, - shippingAddress: Object, + paymentToken: Object, + shippingAddress?: Object, }) { const { + cardInfo, googleTransactionId, - paymentDescription + paymentToken, } = details; return { + cardInfo, googleTransactionId, - paymentDescription, - // On Android, the recommended flow is to have user's confirm prior to - // retrieving the full wallet. - getPaymentToken: () => NativePayments.getFullWalletAndroid( - googleTransactionId, - getPlatformMethodData(JSON.parse(this._serializedMethodData, Platform.OS)), - convertDetailAmountsToString(this._details) - ) + paymentToken, }; } _handleUserAccept(details: { - transactionIdentifier: string, - paymentData: string, - shippingAddress: Object, - payerEmail: string, - paymentToken?: string, + cardInfo?: Object, + googleTransactionId?: string, + transactionIdentifier?: string, + paymentData?: Object, + shippingAddress?: Object, + payerEmail?: string, + paymentToken: Object | string, }) { // On Android, we don't have `onShippingAddressChange` events, so we // set the shipping address when the user accepts. @@ -469,12 +467,5 @@ export default class PaymentRequest { }); }); } - - // https://www.w3.org/TR/payment-request/#canmakepayment-method - canMakePayments(): Promise { - return NativePayments.canMakePayments( - getPlatformMethodData(JSON.parse(this._serializedMethodData), Platform.OS) - ); - } } diff --git a/packages/react-native-payments/lib/js/index.js b/packages/react-native-payments/lib/js/index.js index 3fa6f02d..97b90486 100644 --- a/packages/react-native-payments/lib/js/index.js +++ b/packages/react-native-payments/lib/js/index.js @@ -1,7 +1,9 @@ // @flow import _PaymentRequest from './PaymentRequest'; +import _NativePayments from './NativePayments'; import { PKPaymentButton } from './PKPaymentButton'; export const ApplePayButton = PKPaymentButton; export const PaymentRequest = _PaymentRequest; +export const canMakePayments = _NativePayments.canMakePayments; \ No newline at end of file diff --git a/packages/react-native-payments/lib/js/types.js b/packages/react-native-payments/lib/js/types.js index 8608a66f..eabbca32 100644 --- a/packages/react-native-payments/lib/js/types.js +++ b/packages/react-native-payments/lib/js/types.js @@ -97,3 +97,9 @@ export type PaymentDetailsIOSRaw = { paymentToken?: string, transactionIdentifier: string, }; + +export type CanMakePayments = { + supportedNetworks: Array, + allowedPaymentMethods: Array, + environment: string +};