diff --git a/android/build.gradle b/android/build.gradle index d155eb0a6..5aa582948 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,7 +27,8 @@ dependencies { compile 'com.facebook.react:react-native:+' compile 'com.android.support:support-v4:26.1.0' compile 'com.android.support:appcompat-v7:26.1.0' - compile 'com.google.android.gms:play-services-wallet:10.+' + compile 'com.google.android.gms:play-services-wallet:11.8.0' + compile 'com.google.firebase:firebase-core:11.8.0' compile 'com.stripe:stripe-android:6.0.0' compile 'com.github.tipsi:CreditCardEntry:1.4.8.10' } diff --git a/android/src/main/java/com/gettipsi/stripe/CustomCardInputReactManager.java b/android/src/main/java/com/gettipsi/stripe/CustomCardInputReactManager.java index 4e2bfcbcb..a994fab02 100644 --- a/android/src/main/java/com/gettipsi/stripe/CustomCardInputReactManager.java +++ b/android/src/main/java/com/gettipsi/stripe/CustomCardInputReactManager.java @@ -145,7 +145,7 @@ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { try { currentMonth = view.getCreditCard().getExpMonth(); }catch (Exception e){ - if(charSequence.length() == 0) + if (charSequence.length() == 0) currentMonth = 0; } try { diff --git a/android/src/main/java/com/gettipsi/stripe/GoogleApiPayFlowImpl.java b/android/src/main/java/com/gettipsi/stripe/GoogleApiPayFlowImpl.java new file mode 100644 index 000000000..659d63d82 --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/GoogleApiPayFlowImpl.java @@ -0,0 +1,237 @@ +package com.gettipsi.stripe; + +import android.app.Activity; +import android.content.Intent; +import android.support.annotation.NonNull; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableMap; +import com.gettipsi.stripe.util.ArgCheck; +import com.gettipsi.stripe.util.Converters; +import com.gettipsi.stripe.util.Fun0; +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.google.android.gms.wallet.AutoResolveHelper; +import com.google.android.gms.wallet.CardRequirements; +import com.google.android.gms.wallet.IsReadyToPayRequest; +import com.google.android.gms.wallet.PaymentData; +import com.google.android.gms.wallet.PaymentDataRequest; +import com.google.android.gms.wallet.PaymentMethodTokenizationParameters; +import com.google.android.gms.wallet.PaymentsClient; +import com.google.android.gms.wallet.ShippingAddressRequirements; +import com.google.android.gms.wallet.TransactionInfo; +import com.google.android.gms.wallet.Wallet; +import com.google.android.gms.wallet.WalletConstants; +import com.stripe.android.BuildConfig; +import com.stripe.android.model.Token; + +import java.util.Arrays; +import java.util.Collection; + +import static com.gettipsi.stripe.util.Converters.convertTokenToWritableMap; +import static com.gettipsi.stripe.util.Converters.getAllowedShippingCountryCodes; +import static com.gettipsi.stripe.util.Converters.getBillingAddress; +import static com.gettipsi.stripe.util.Converters.putExtraToTokenMap; +import static com.gettipsi.stripe.util.PayParams.BILLING_ADDRESS_REQUIRED; +import static com.gettipsi.stripe.util.PayParams.CURRENCY_CODE; +import static com.gettipsi.stripe.util.PayParams.SHIPPING_ADDRESS_REQUIRED; +import static com.gettipsi.stripe.util.PayParams.TOTAL_PRICE; + +/** + * Created by ngoriachev on 13/03/2018. + * see https://developers.google.com/pay/api/tutorial + */ +public final class GoogleApiPayFlowImpl extends PayFlow { + + private static final String TAG = GoogleApiPayFlowImpl.class.getSimpleName(); + private static final int LOAD_PAYMENT_DATA_REQUEST_CODE = 100500; + + private PaymentsClient mPaymentsClient; + private Promise payPromise; + + public GoogleApiPayFlowImpl(@NonNull Fun0 activityProvider) { + super(activityProvider); + } + + private PaymentsClient createPaymentsClient(@NonNull Activity activity) { + return Wallet.getPaymentsClient( + activity, + new Wallet.WalletOptions.Builder().setEnvironment(getEnvironment()).build()); + } + + private void isReadyToPay(@NonNull Activity activity, @NonNull final Promise promise) { + ArgCheck.nonNull(activity); + ArgCheck.nonNull(promise); + + IsReadyToPayRequest request = + IsReadyToPayRequest.newBuilder() + .addAllowedPaymentMethod(WalletConstants.PAYMENT_METHOD_CARD) + .addAllowedPaymentMethod(WalletConstants.PAYMENT_METHOD_TOKENIZED_CARD) + .build(); + mPaymentsClient = createPaymentsClient(activity); + Task task = mPaymentsClient.isReadyToPay(request); + task.addOnCompleteListener( + new OnCompleteListener() { + public void onComplete(Task task) { + try { + boolean result = task.getResult(ApiException.class); + promise.resolve(result); + } catch (ApiException exception) { + promise.reject(TAG, String.format("Error, statusCode: %d", exception.getStatusCode())); + } + } + }); + } + + private PaymentMethodTokenizationParameters createPaymentMethodTokenizationParameters() { + return PaymentMethodTokenizationParameters.newBuilder() + .setPaymentMethodTokenizationType(WalletConstants.PAYMENT_METHOD_TOKENIZATION_TYPE_PAYMENT_GATEWAY) + .addParameter("gateway", "stripe") + .addParameter("stripe:publishableKey", getPublishableKey()) + .addParameter("stripe:version", BuildConfig.VERSION_NAME) + .build(); + } + + private PaymentDataRequest createPaymentDataRequest(ReadableMap payParams) { + final String estimatedTotalPrice = payParams.getString(TOTAL_PRICE); + final String currencyCode = payParams.getString(CURRENCY_CODE); + final boolean billingAddressRequired = Converters.getValue(payParams, BILLING_ADDRESS_REQUIRED, false); + final Boolean shippingAddressRequired = Converters.getValue(payParams, SHIPPING_ADDRESS_REQUIRED, false); + final Collection allowedCountryCodes = getAllowedShippingCountryCodes(payParams); + + return createPaymentDataRequest(estimatedTotalPrice, currencyCode, billingAddressRequired, shippingAddressRequired, allowedCountryCodes); + } + + private PaymentDataRequest createPaymentDataRequest(@NonNull final String totalPrice, + @NonNull final String currencyCode, + final boolean billingAddressRequired, + final boolean shippingAddressRequired, + @NonNull final Collection countryCodes + ) { + + ArgCheck.isDouble(totalPrice); + ArgCheck.notEmptyString(currencyCode); + + PaymentDataRequest.Builder builder = PaymentDataRequest.newBuilder(); + builder.setTransactionInfo( + TransactionInfo.newBuilder() + .setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_ESTIMATED) + .setTotalPrice(totalPrice) + .setCurrencyCode(currencyCode) + .build()); + + builder + .setCardRequirements( + CardRequirements.newBuilder() + .addAllowedCardNetworks( + Arrays.asList( + WalletConstants.CARD_NETWORK_AMEX, + WalletConstants.CARD_NETWORK_DISCOVER, + WalletConstants.CARD_NETWORK_VISA, + WalletConstants.CARD_NETWORK_MASTERCARD)) + .setBillingAddressRequired(billingAddressRequired) + .build()) + .addAllowedPaymentMethod(WalletConstants.PAYMENT_METHOD_CARD) + .addAllowedPaymentMethod(WalletConstants.PAYMENT_METHOD_TOKENIZED_CARD) + .setShippingAddressRequired(shippingAddressRequired); + + if (countryCodes.size() > 0) { + builder.setShippingAddressRequirements( + ShippingAddressRequirements.newBuilder() + .addAllowedCountryCodes(countryCodes) + .build()); + } + + builder.setPaymentMethodTokenizationParameters(createPaymentMethodTokenizationParameters()); + return builder.build(); + } + + private void startPaymentRequest(@NonNull Activity activity, @NonNull PaymentDataRequest request) { + ArgCheck.nonNull(activity); + ArgCheck.nonNull(request); + + mPaymentsClient = createPaymentsClient(activity); + + AutoResolveHelper.resolveTask( + mPaymentsClient.loadPaymentData(request), + activity, + LOAD_PAYMENT_DATA_REQUEST_CODE); + } + + @Override + public void paymentRequestWithAndroidPay(@NonNull ReadableMap payParams, @NonNull Promise promise) { + ArgCheck.nonNull(payParams); + ArgCheck.nonNull(promise); + + Activity activity = activityProvider.call(); + if (activity == null) { + promise.reject(TAG, NO_CURRENT_ACTIVITY_MSG); + return; + } + + this.payPromise = promise; + startPaymentRequest(activity, createPaymentDataRequest(payParams)); + } + + @Override + public void deviceSupportsAndroidPay(@NonNull Promise promise) { + Activity activity = activityProvider.call(); + if (activity == null) { + promise.reject(TAG, NO_CURRENT_ACTIVITY_MSG); + return; + } + + if (!isPlayServicesAvailable(activity)) { + promise.reject(TAG, PLAY_SERVICES_ARE_NOT_AVAILABLE_MSG); + return; + } + + isReadyToPay(activity, promise); + } + + public boolean onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (payPromise == null) { + return false; + } + + switch (requestCode) { + case LOAD_PAYMENT_DATA_REQUEST_CODE: + switch (resultCode) { + case Activity.RESULT_OK: + PaymentData paymentData = PaymentData.getFromIntent(data); + ArgCheck.nonNull(paymentData); + String tokenJson = paymentData.getPaymentMethodToken().getToken(); + Token token = Token.fromString(tokenJson); + if (token == null) { + payPromise.reject(TAG, JSON_PARSING_ERROR_MSG); + } else { + payPromise.resolve(putExtraToTokenMap( + convertTokenToWritableMap(token), + getBillingAddress(paymentData), + paymentData.getShippingAddress())); + } + break; + case Activity.RESULT_CANCELED: + payPromise.reject(TAG, PURCHASE_CANCELLED_MSG); + break; + case AutoResolveHelper.RESULT_ERROR: + Status status = AutoResolveHelper.getStatusFromIntent(data); + // Log the status for debugging. + // Generally, there is no need to show an error to + // the user as the Google Pay API will do that. + payPromise.reject(TAG, status.getStatusMessage()); + break; + + default: + // Do nothing. + } + payPromise = null; + return true; + } + + return false; + } + +} diff --git a/android/src/main/java/com/gettipsi/stripe/ObsoleteApiPayFlowImpl.java b/android/src/main/java/com/gettipsi/stripe/ObsoleteApiPayFlowImpl.java new file mode 100644 index 000000000..0ab200046 --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/ObsoleteApiPayFlowImpl.java @@ -0,0 +1,319 @@ +package com.gettipsi.stripe; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.gettipsi.stripe.util.Action; +import com.gettipsi.stripe.util.Converters; +import com.gettipsi.stripe.util.Fun0; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.BooleanResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.identity.intents.model.CountrySpecification; +import com.google.android.gms.wallet.Cart; +import com.google.android.gms.wallet.FullWallet; +import com.google.android.gms.wallet.FullWalletRequest; +import com.google.android.gms.wallet.IsReadyToPayRequest; +import com.google.android.gms.wallet.LineItem; +import com.google.android.gms.wallet.MaskedWallet; +import com.google.android.gms.wallet.MaskedWalletRequest; +import com.google.android.gms.wallet.PaymentMethodTokenizationParameters; +import com.google.android.gms.wallet.PaymentMethodTokenizationType; +import com.google.android.gms.wallet.Wallet; +import com.google.android.gms.wallet.WalletConstants; +import com.stripe.android.model.Token; + +import java.util.ArrayList; + +import static com.gettipsi.stripe.util.Converters.convertTokenToWritableMap; +import static com.gettipsi.stripe.util.Converters.getAllowedShippingCountries; +import static com.gettipsi.stripe.util.Converters.putExtraToTokenMap; +import static com.gettipsi.stripe.util.PayParams.CURRENCY_CODE; +import static com.gettipsi.stripe.util.PayParams.DESCRIPTION; +import static com.gettipsi.stripe.util.PayParams.LINE_ITEMS; +import static com.gettipsi.stripe.util.PayParams.QUANTITY; +import static com.gettipsi.stripe.util.PayParams.SHIPPING_ADDRESS_REQUIRED; +import static com.gettipsi.stripe.util.PayParams.TOTAL_PRICE; +import static com.gettipsi.stripe.util.PayParams.UNIT_PRICE; + +/** + * Created by ngoriachev on 13/03/2018 + */ + +public class ObsoleteApiPayFlowImpl extends PayFlow { + + public static final String TAG = ObsoleteApiPayFlowImpl.class.getSimpleName(); + + private static final int LOAD_MASKED_WALLET_REQUEST_CODE = 100502; + private static final int LOAD_FULL_WALLET_REQUEST_CODE = 100503; + + private Promise mPayPromise; + private GoogleApiClient mGoogleApiClient; + private ReadableMap mAndroidPayParams; + + public ObsoleteApiPayFlowImpl(@NonNull Fun0 activityProvider) { + super(activityProvider); + } + + @Override + public void deviceSupportsAndroidPay(final Promise promise) { + Activity activity = activityProvider.call(); + if (activity == null) { + promise.reject(TAG, NO_CURRENT_ACTIVITY_MSG); + return; + } + + if (!isPlayServicesAvailable(activity)) { + promise.reject(TAG, PLAY_SERVICES_ARE_NOT_AVAILABLE_MSG); + return; + } + + connectAndCheckIsWalletReadyToPay(promise); + } + + @Override + public boolean onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (mPayPromise == null) { + return false; + } + + switch (requestCode) { + case LOAD_MASKED_WALLET_REQUEST_CODE: + handleLoadMascedWaletRequest(resultCode, data); + return true; + case LOAD_FULL_WALLET_REQUEST_CODE: + handleLoadFullWalletRequest(resultCode, data); + return true; + default: + return false; + } + + } + + @Override + public void paymentRequestWithAndroidPay(final ReadableMap payParams, final Promise promise) { + mPayPromise = promise; + connectAndCheckIsWalletReadyToPayAndLoadMaskedWallet(payParams); + } + + private void handleLoadFullWalletRequest(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + FullWallet fullWallet = data.getParcelableExtra(WalletConstants.EXTRA_FULL_WALLET); + String tokenJSON = fullWallet.getPaymentMethodToken().getToken(); + Token token = Token.fromString(tokenJSON); + if (token == null) { + // Log the error and notify Stripe help + mPayPromise.reject(TAG, JSON_PARSING_ERROR_MSG); + } else { + mPayPromise.resolve(putExtraToTokenMap( + convertTokenToWritableMap(token), + fullWallet.getBuyerBillingAddress(), + fullWallet.getBuyerShippingAddress())); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + mPayPromise.reject(TAG, PURCHASE_CANCELLED_MSG); + } else { + mPayPromise.reject(TAG, PURCHASE_LOAD_FULL_WALLET_ERROR_MSG); + } + disconnect(mGoogleApiClient); + } + + private void connectAndCheckIsWalletReadyToPay(final Promise promise) { + Activity activity = activityProvider.call(); + if (activity == null) { + promise.reject(TAG, NO_CURRENT_ACTIVITY_MSG); + return; + } + + mGoogleApiClient = buildGoogleApiClientWithConnectedCallback( + activity, + getEnvironment(), + new Action() { public void call(String errorMsg) { + promise.reject(TAG, errorMsg); + disconnect(mGoogleApiClient); + }}, + new Action() { public void call(Bundle bundle) { + isWalletReadyToPay(mGoogleApiClient, promise); + }}); + mGoogleApiClient.connect(); + } + + private void connectAndCheckIsWalletReadyToPayAndLoadMaskedWallet(final ReadableMap payParams) { + final Activity activity = activityProvider.call(); + if (activity == null) { + mPayPromise.reject(TAG, NO_CURRENT_ACTIVITY_MSG); + return; + } + + mGoogleApiClient = buildGoogleApiClientWithConnectedCallback( + activity, + getEnvironment(), + new Action() { public void call(String errorMsg) { + mPayPromise.reject(TAG, errorMsg); + disconnect(mGoogleApiClient); + }}, + new Action() { public void call(Bundle bundle) { + checkIsWalletReadyToPayAndLoadMaskedWallet(mGoogleApiClient, mPayPromise, payParams, activity); + }}); + mGoogleApiClient.connect(); + } + + private static void disconnect(GoogleApiClient googleApiClient) { + if (googleApiClient != null) { + googleApiClient.disconnect(); + } + } + + private void handleLoadMascedWaletRequest(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + + MaskedWallet maskedWallet = data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET); + + final Cart.Builder cartBuilder = Cart.newBuilder() + .setCurrencyCode(mAndroidPayParams.getString(CURRENCY_CODE)) + .setTotalPrice(mAndroidPayParams.getString(TOTAL_PRICE)); + + final ReadableArray lineItems = mAndroidPayParams.getArray(LINE_ITEMS); + if (lineItems != null) { + for (int i = 0; i < lineItems.size(); i++) { + final ReadableMap lineItem = lineItems.getMap(i); + cartBuilder.addLineItem(LineItem.newBuilder() // Identify item being purchased + .setCurrencyCode(lineItem.getString(CURRENCY_CODE)) + .setQuantity(lineItem.getString(QUANTITY)) + .setDescription(DESCRIPTION) + .setTotalPrice(TOTAL_PRICE) + .setUnitPrice(UNIT_PRICE) + .build()); + } + } + + final FullWalletRequest fullWalletRequest = FullWalletRequest.newBuilder() + .setCart(cartBuilder.build()) + .setGoogleTransactionId(maskedWallet.getGoogleTransactionId()) + .build(); + + Wallet.Payments.loadFullWallet(mGoogleApiClient, fullWalletRequest, LOAD_FULL_WALLET_REQUEST_CODE); + } else { + mPayPromise.reject(TAG, PURCHASE_LOAD_MASKED_WALLET_ERROR_MSG); + disconnect(mGoogleApiClient); + } + } + + private static void isWalletReadyToPay(final GoogleApiClient googleApiClient, final Promise promise) { + isWalletReadyToPay( + googleApiClient, + new ResultCallback() { public void onResult(@NonNull BooleanResult result) { + if (result.getStatus().isSuccess()) { + promise.resolve(result.getValue()); + } else { + promise.reject(TAG, result.getStatus().getStatusMessage()); + } + disconnect(googleApiClient); + }}); + } + + private static void isWalletReadyToPay(GoogleApiClient gac, ResultCallback callback) { + Wallet.Payments.isReadyToPay(gac, IsReadyToPayRequest.newBuilder().build()).setResultCallback(callback); + } + + private void checkIsWalletReadyToPayAndLoadMaskedWallet(final GoogleApiClient googleApiClient, + final Promise promise, + final ReadableMap payParams, + final Activity activity + ) { + isWalletReadyToPay( + googleApiClient, + new ResultCallback() { + @Override + public void onResult(@NonNull BooleanResult result) { + if (result.getStatus().isSuccess()) { + if (result.getValue()) { + mAndroidPayParams = payParams; + final String estimatedTotalPrice = payParams.getString(TOTAL_PRICE); + final String currencyCode = payParams.getString(CURRENCY_CODE); + final Boolean shippingAddressRequired = Converters.getValue(payParams, SHIPPING_ADDRESS_REQUIRED, true); + final ArrayList allowedCountries = getAllowedShippingCountries(payParams); + final MaskedWalletRequest maskedWalletRequest = createWalletRequest(estimatedTotalPrice, currencyCode, shippingAddressRequired, allowedCountries); + Wallet.Payments.loadMaskedWallet(googleApiClient, maskedWalletRequest, LOAD_MASKED_WALLET_REQUEST_CODE); + } else { + androidPayUnavailableDialog(activity); + promise.reject(TAG, ANDROID_PAY_UNAVAILABLE_ERROR_MSG); + disconnect(googleApiClient); + } + } else { + androidPayUnavailableDialog(activity); + promise.reject(TAG, MAKING_IS_READY_TO_PAY_CALL_ERROR_MSG); + disconnect(googleApiClient); + } + } + }); + } + + private static GoogleApiClient buildGoogleApiClientWithConnectedCallback(Activity activity, + int env, + final Action onError, + final Action onConnected) { + GoogleApiClient googleApiClient = new GoogleApiClient.Builder(activity) + .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { + @Override + public void onConnected(@Nullable Bundle bundle) { + onConnected.call(bundle); + } + + @Override + public void onConnectionSuspended(int i) { + onError.call("onConnectionSuspended i = " + i); + } + }) + .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() { + @Override + public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { + onError.call("onConnectionFailed: " + connectionResult.getErrorMessage()); + } + }) + .addApi(Wallet.API, new Wallet.WalletOptions.Builder() + .setEnvironment(env) + .setTheme(WalletConstants.THEME_LIGHT) + .build()) + .build(); + return googleApiClient; + } + + private MaskedWalletRequest createWalletRequest(final String estimatedTotalPrice, + final String currencyCode, + final Boolean shippingAddressRequired, + final ArrayList countries) { + final MaskedWalletRequest maskedWalletRequest = MaskedWalletRequest.newBuilder() + + // Request credit card tokenization with Stripe by specifying tokenization parameters: + .setPaymentMethodTokenizationParameters(PaymentMethodTokenizationParameters.newBuilder() + .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.PAYMENT_GATEWAY) + .addParameter("gateway", "stripe") + .addParameter("stripe:publishableKey", getPublishableKey()) + .addParameter("stripe:version", com.stripe.android.BuildConfig.VERSION_NAME) + .build()) + // You want the shipping address: + .setShippingAddressRequired(shippingAddressRequired) + .addAllowedCountrySpecificationsForShipping(countries) + // Price set as a decimal: + .setEstimatedTotalPrice(estimatedTotalPrice) + .setCurrencyCode(currencyCode) + .build(); + return maskedWalletRequest; + } + + private void androidPayUnavailableDialog(@NonNull Activity activity) { + new AlertDialog.Builder(activity) + .setMessage(R.string.gettipsi_android_pay_unavaliable) + .setPositiveButton(android.R.string.ok, null) + .show(); + } +} diff --git a/android/src/main/java/com/gettipsi/stripe/PayFlow.java b/android/src/main/java/com/gettipsi/stripe/PayFlow.java new file mode 100644 index 000000000..ea10510c1 --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/PayFlow.java @@ -0,0 +1,98 @@ +package com.gettipsi.stripe; + +import android.app.Activity; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableMap; +import com.gettipsi.stripe.util.ArgCheck; +import com.gettipsi.stripe.util.Fun0; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.wallet.WalletConstants; + +public abstract class PayFlow { + + public static final String NO_CURRENT_ACTIVITY_MSG = "Cannot start process with no current activity"; + public static final String PURCHASE_CANCELLED_MSG = "Purchase was cancelled"; + public static final String PURCHASE_LOAD_MASKED_WALLET_ERROR_MSG = "Purchase masked wallet error"; + public static final String PURCHASE_LOAD_FULL_WALLET_ERROR_MSG = "Purchase full wallet error"; + public static final String ANDROID_PAY_UNAVAILABLE_ERROR_MSG = "Android Pay is unavailable"; + public static final String MAKING_IS_READY_TO_PAY_CALL_ERROR_MSG = "Error making isReadyToPay call"; + public static final String JSON_PARSING_ERROR_MSG = "Failed to create token from JSON string"; + public static final String PLAY_SERVICES_ARE_NOT_AVAILABLE_MSG = "Play services are not available!"; + + private static final boolean useObsoleteFlow = false; + + protected final @NonNull Fun0 activityProvider; + private String publishableKey; // invalid value by default + private int environment; // invalid value by default + + + public PayFlow(@NonNull Fun0 activityProvider) { + ArgCheck.nonNull(activityProvider); + this.activityProvider = activityProvider; + } + + public static PayFlow create(Fun0 activityProvider) { + if (!useObsoleteFlow) { + return new GoogleApiPayFlowImpl(activityProvider); + } else { + return new ObsoleteApiPayFlowImpl(activityProvider); + } + } + + private static boolean isValidEnvironment(int environment) { + return environment == WalletConstants.ENVIRONMENT_TEST || + environment == WalletConstants.ENVIRONMENT_PRODUCTION; + } + + private static boolean isEnvironmentChangeAttempt(int oldEnvironment, int newEnvironment) { + return oldEnvironment != newEnvironment && isValidEnvironment(oldEnvironment) && + isValidEnvironment(newEnvironment); + } + + protected int getEnvironment() { + ArgCheck.isTrue(isValidEnvironment(environment)); + + return environment; + } + + public void setEnvironment(int environment) { + ArgCheck.isTrue(isValidEnvironment(environment)); + ArgCheck.isTrue(!isEnvironmentChangeAttempt(this.environment, environment)); + + this.environment = environment; + } + + protected String getPublishableKey() { + return ArgCheck.notEmptyString(publishableKey); + } + + public void setPublishableKey(@NonNull String publishableKey) { + this.publishableKey = ArgCheck.notEmptyString(publishableKey); + } + + abstract void paymentRequestWithAndroidPay(final ReadableMap payParams, final Promise promise); + + abstract void deviceSupportsAndroidPay(final Promise promise); + + abstract boolean onActivityResult(Activity activity, int requestCode, int resultCode, Intent data); + + public static boolean isPlayServicesAvailable(@NonNull Activity activity) { + ArgCheck.nonNull(activity); + + GoogleApiAvailability googleAPI = GoogleApiAvailability.getInstance(); + int result = googleAPI.isGooglePlayServicesAvailable(activity); + + return result == ConnectionResult.SUCCESS; + } + + protected static void log(String TAG, String msg) { + if (BuildConfig.DEBUG) { + Log.d(TAG, msg); + } + } +} diff --git a/android/src/main/java/com/gettipsi/stripe/StripeModule.java b/android/src/main/java/com/gettipsi/stripe/StripeModule.java index 65bc738a3..0d31bdc81 100644 --- a/android/src/main/java/com/gettipsi/stripe/StripeModule.java +++ b/android/src/main/java/com/gettipsi/stripe/StripeModule.java @@ -1,145 +1,76 @@ package com.gettipsi.stripe; import android.app.Activity; -import android.app.AlertDialog; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; -import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import android.text.TextUtils; import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.BaseActivityEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeArray; -import com.facebook.react.bridge.WritableNativeMap; import com.gettipsi.stripe.dialog.AddCardDialogFragment; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.common.api.BooleanResult; -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.wallet.Cart; -import com.google.android.gms.wallet.FullWallet; -import com.google.android.gms.wallet.FullWalletRequest; -import com.google.android.gms.wallet.IsReadyToPayRequest; -import com.google.android.gms.wallet.LineItem; -import com.google.android.gms.wallet.MaskedWallet; -import com.google.android.gms.wallet.MaskedWalletRequest; -import com.google.android.gms.wallet.PaymentMethodTokenizationParameters; -import com.google.android.gms.wallet.PaymentMethodTokenizationType; -import com.google.android.gms.wallet.Wallet; +import com.gettipsi.stripe.util.ArgCheck; +import com.gettipsi.stripe.util.Converters; +import com.gettipsi.stripe.util.Fun0; import com.google.android.gms.wallet.WalletConstants; -import com.google.android.gms.identity.intents.model.UserAddress; -import com.google.android.gms.identity.intents.model.CountrySpecification; -import com.stripe.android.BuildConfig; import com.stripe.android.SourceCallback; -import com.google.android.gms.identity.intents.model.CountrySpecification; import com.stripe.android.Stripe; import com.stripe.android.TokenCallback; -import com.stripe.android.exception.APIConnectionException; -import com.stripe.android.exception.APIException; -import com.stripe.android.exception.AuthenticationException; -import com.stripe.android.exception.CardException; -import com.stripe.android.exception.InvalidRequestException; -import com.stripe.android.model.Address; -import com.stripe.android.model.BankAccount; -import com.stripe.android.model.Card; import com.stripe.android.model.Source; -import com.stripe.android.model.SourceCodeVerification; -import com.stripe.android.model.SourceOwner; import com.stripe.android.model.SourceParams; -import com.stripe.android.model.SourceReceiver; -import com.stripe.android.model.SourceRedirect; import com.stripe.android.model.Token; -import java.util.Map; - -import java.util.List; -import java.util.ArrayList; +import static com.gettipsi.stripe.PayFlow.NO_CURRENT_ACTIVITY_MSG; +import static com.gettipsi.stripe.util.Converters.convertSourceToWritableMap; +import static com.gettipsi.stripe.util.Converters.convertTokenToWritableMap; +import static com.gettipsi.stripe.util.Converters.createBankAccount; +import static com.gettipsi.stripe.util.Converters.createCard; +import static com.gettipsi.stripe.util.Converters.getStringOrNull; +import static com.gettipsi.stripe.util.InitializationOptions.ANDROID_PAY_MODE_KEY; +import static com.gettipsi.stripe.util.InitializationOptions.ANDROID_PAY_MODE_PRODUCTION; +import static com.gettipsi.stripe.util.InitializationOptions.PUBLISHABLE_KEY; +import static com.gettipsi.stripe.util.InitializationOptions.ANDROID_PAY_MODE_TEST; public class StripeModule extends ReactContextBaseJavaModule { + private static final String MODULE_NAME = StripeModule.class.getSimpleName(); + private static final String TAG = "### " + MODULE_NAME + ": "; - private static final String TAG = StripeModule.class.getSimpleName(); - private static final String MODULE_NAME = "StripeModule"; - - private static final int LOAD_MASKED_WALLET_REQUEST_CODE = 100502; - private static final int LOAD_FULL_WALLET_REQUEST_CODE = 100503; - - private static final String PURCHASE_CANCELLED = "PURCHASE_CANCELLED"; - - //androidPayParams keys: - private static final String ANDROID_PAY_MODE = "androidPayMode"; - private static final String PRODUCTION = "production"; - private static final String CURRENCY_CODE = "currency_code"; - private static final String SHIPPING_ADDRESS_REQUIRED = "shipping_address_required"; - private static final String TOTAL_PRICE = "total_price"; - private static final String UNIT_PRICE = "unit_price"; - private static final String LINE_ITEMS = "line_items"; - private static final String QUANTITY = "quantity"; - private static final String DESCRIPTION = "description"; - - private int mEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION; - - private static StripeModule instance = null; + private static StripeModule sInstance = null; public static StripeModule getInstance() { - return instance; + return sInstance; } public Stripe getStripe() { - return stripe; + return mStripe; } @Nullable - private Promise createSourcePromise; - private Promise payPromise; + private Promise mCreateSourcePromise; @Nullable - private Source createdSource; + private Source mCreatedSource; - private String publicKey; - private Stripe stripe; - private GoogleApiClient googleApiClient; + private String mPublicKey; + private Stripe mStripe; + private PayFlow mPayFlow; - private ReadableMap androidPayParams; private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() { @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - if (payPromise != null) { - if (requestCode == LOAD_MASKED_WALLET_REQUEST_CODE) { // Unique, identifying constant - - handleLoadMascedWaletRequest(resultCode, data); - - } else if (requestCode == LOAD_FULL_WALLET_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK) { - FullWallet fullWallet = data.getParcelableExtra(WalletConstants.EXTRA_FULL_WALLET); - String tokenJSON = fullWallet.getPaymentMethodToken().getToken(); - Token token = Token.fromString(tokenJSON); - if (token == null) { - // Log the error and notify Stripe help - Log.e(TAG, "onActivityResult: failed to create token from JSON string."); - payPromise.reject("JsonParsingError", "Failed to create token from JSON string."); - } else { - payPromise.resolve(convertTokenToWritableMap(token)); - } - } - } else { - super.onActivityResult(activity, requestCode, resultCode, data); - } + boolean handled = getPayFlow().onActivityResult(activity, requestCode, resultCode, data); + if (!handled) { + super.onActivityResult(activity, requestCode, resultCode, data); } } }; @@ -151,7 +82,7 @@ public StripeModule(ReactApplicationContext reactContext) { // Add the listener for `onActivityResult` reactContext.addActivityEventListener(mActivityEventListener); - instance = this; + sInstance = this; } @Override @@ -160,82 +91,62 @@ public String getName() { } @ReactMethod - public void init(ReadableMap options) { - if(exist(options, ANDROID_PAY_MODE, PRODUCTION).toLowerCase().equals("test")) { - mEnvironment = WalletConstants.ENVIRONMENT_TEST; - Log.d(TAG, "Environment: test mode"); + public void init(@NonNull ReadableMap options) { + ArgCheck.nonNull(options); + + String newPubKey = Converters.getStringOrNull(options, PUBLISHABLE_KEY); + String newAndroidPayMode = Converters.getStringOrNull(options, ANDROID_PAY_MODE_KEY); + + if (newPubKey != null && !TextUtils.equals(newPubKey, mPublicKey)) { + ArgCheck.notEmptyString(newPubKey); + + mPublicKey = newPubKey; + mStripe = new Stripe(getReactApplicationContext(), mPublicKey); + getPayFlow().setPublishableKey(mPublicKey); } - publicKey = options.getString("publishableKey"); + if (newAndroidPayMode != null) { + ArgCheck.isTrue(ANDROID_PAY_MODE_TEST.equals(newAndroidPayMode) || ANDROID_PAY_MODE_PRODUCTION.equals(newAndroidPayMode)); - stripe = new Stripe(getReactApplicationContext(), publicKey); + getPayFlow().setEnvironment(androidPayModeToEnvironment(newAndroidPayMode)); + } } - @ReactMethod - public void deviceSupportsAndroidPay(final Promise promise) { - if (!isPlayServicesAvailable()) { - promise.reject(TAG, "Play services are not available!"); - return; + private PayFlow getPayFlow() { + if (mPayFlow == null) { + mPayFlow = PayFlow.create( + new Fun0() { public Activity call() { + return getCurrentActivity(); + }} + ); } - if (googleApiClient != null && googleApiClient.isConnected()) { - checkAndroidPayAvaliable(googleApiClient, promise); - } else if (googleApiClient != null && !googleApiClient.isConnected()) { - googleApiClient.registerConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { - @Override - public void onConnected(@Nullable Bundle bundle) { - checkAndroidPayAvaliable(googleApiClient, promise); - } - @Override - public void onConnectionSuspended(int i) { - promise.reject(TAG, "onConnectionSuspended i = " + i); - } - }); - googleApiClient.connect(); - } else if (googleApiClient == null && getCurrentActivity() != null) { - googleApiClient = new GoogleApiClient.Builder(getCurrentActivity()) - .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { - @Override - public void onConnected(@Nullable Bundle bundle) { - Log.d(TAG, "onConnected: "); - checkAndroidPayAvaliable(googleApiClient, promise); - } + return mPayFlow; + } - @Override - public void onConnectionSuspended(int i) { - Log.d(TAG, "onConnectionSuspended: "); - promise.reject(TAG, "onConnectionSuspended i = " + i); - } - }) - .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() { - @Override - public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { - Log.d(TAG, "onConnectionFailed: "); - promise.reject(TAG, "onConnectionFailed: " + connectionResult.getErrorMessage()); - } - }) - .addApi(Wallet.API, new Wallet.WalletOptions.Builder() - .setEnvironment(mEnvironment) - .setTheme(WalletConstants.THEME_LIGHT) - .build()) - .build(); - googleApiClient.connect(); - } else { - promise.reject(TAG, "Unknown error"); - } + private static int androidPayModeToEnvironment(@NonNull String androidPayMode) { + ArgCheck.notEmptyString(androidPayMode); + return ANDROID_PAY_MODE_TEST.equals(androidPayMode.toLowerCase()) ? WalletConstants.ENVIRONMENT_TEST : WalletConstants.ENVIRONMENT_PRODUCTION; + } + + @ReactMethod + public void deviceSupportsAndroidPay(final Promise promise) { + getPayFlow().deviceSupportsAndroidPay(promise); } @ReactMethod public void createTokenWithCard(final ReadableMap cardData, final Promise promise) { try { + ArgCheck.nonNull(mStripe); + ArgCheck.notEmptyString(mPublicKey); - stripe.createToken(createCard(cardData), - publicKey, + mStripe.createToken( + createCard(cardData), + mPublicKey, new TokenCallback() { public void onSuccess(Token token) { promise.resolve(convertTokenToWritableMap(token)); } - public void onError(Exception error) { error.printStackTrace(); promise.reject(TAG, error.getMessage()); @@ -249,14 +160,17 @@ public void onError(Exception error) { @ReactMethod public void createTokenWithBankAccount(final ReadableMap accountData, final Promise promise) { try { - stripe.createBankAccountToken(createBankAccount(accountData), - publicKey, + ArgCheck.nonNull(mStripe); + ArgCheck.notEmptyString(mPublicKey); + + mStripe.createBankAccountToken( + createBankAccount(accountData), + mPublicKey, null, new TokenCallback() { public void onSuccess(Token token) { promise.resolve(convertTokenToWritableMap(token)); } - public void onError(Exception error) { error.printStackTrace(); promise.reject(TAG, error.getMessage()); @@ -269,21 +183,22 @@ public void onError(Exception error) { @ReactMethod public void paymentRequestWithCardForm(ReadableMap unused, final Promise promise) { - if (getCurrentActivity() != null) { - final AddCardDialogFragment cardDialog = AddCardDialogFragment.newInstance(publicKey); + Activity currentActivity = getCurrentActivity(); + try { + ArgCheck.nonNull(currentActivity); + ArgCheck.notEmptyString(mPublicKey); + + final AddCardDialogFragment cardDialog = AddCardDialogFragment.newInstance(mPublicKey); cardDialog.setPromise(promise); - cardDialog.show(getCurrentActivity().getFragmentManager(), "AddNewCard"); + cardDialog.show(currentActivity.getFragmentManager(), "AddNewCard"); + } catch (Exception e) { + promise.reject(TAG, e.getMessage()); } } @ReactMethod - public void paymentRequestWithAndroidPay(final ReadableMap map, final Promise promise) { - Log.d(TAG, "startAndroidPay: "); - if (getCurrentActivity() != null) { - payPromise = promise; - Log.d(TAG, "startAndroidPay: getCurrentActivity() != null"); - startApiClientAndAndroidPay(getCurrentActivity(), map); - } + public void paymentRequestWithAndroidPay(final ReadableMap payParams, final Promise promise) { + getPayFlow().paymentRequestWithAndroidPay(payParams, promise); } @ReactMethod @@ -348,10 +263,11 @@ public void createSourceWithParams(final ReadableMap options, final Promise prom options.getString("returnURL"), options.getString("card")); break; - } - stripe.createSource(sourceParams, new SourceCallback() { + ArgCheck.nonNull(sourceParams); + + mStripe.createSource(sourceParams, new SourceCallback() { @Override public void onError(Exception error) { promise.reject(error); @@ -360,16 +276,17 @@ public void onError(Exception error) { @Override public void onSuccess(Source source) { if (Source.REDIRECT.equals(source.getFlow())) { - if (getCurrentActivity() == null) { - promise.reject(TAG, "Cannot start payment process with no current activity"); + Activity currentActivity = getCurrentActivity(); + if (currentActivity == null) { + promise.reject(TAG, NO_CURRENT_ACTIVITY_MSG); } else { - createSourcePromise = promise; - createdSource = source; + mCreateSourcePromise = promise; + mCreatedSource = source; String redirectUrl = source.getRedirect().getUrl(); - Intent browserIntent = new Intent(getCurrentActivity(), OpenBrowserActivity.class) + Intent browserIntent = new Intent(currentActivity, OpenBrowserActivity.class) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP) .putExtra(OpenBrowserActivity.EXTRA_URL, redirectUrl); - getCurrentActivity().startActivity(browserIntent); + currentActivity.startActivity(browserIntent); } } else { promise.resolve(convertSourceToWritableMap(source)); @@ -378,54 +295,50 @@ public void onSuccess(Source source) { }); } - private String getStringOrNull(@NonNull ReadableMap map, @NonNull String key) { - return map.hasKey(key) ? map.getString(key) : null; - } - void processRedirect(@Nullable Uri redirectData) { - if (createdSource == null || createSourcePromise == null) { - Log.d(TAG, "Received redirect uri but there is no source to process"); + if (mCreatedSource == null || mCreateSourcePromise == null) { + return; } if (redirectData == null) { - Log.d(TAG, "Received null `redirectData`"); - createSourcePromise.reject(TAG, "Cancelled"); - createdSource = null; - createSourcePromise = null; + + mCreateSourcePromise.reject(TAG, "Cancelled"); + mCreatedSource = null; + mCreateSourcePromise = null; return; } final String clientSecret = redirectData.getQueryParameter("client_secret"); - if (!createdSource.getClientSecret().equals(clientSecret)) { - createSourcePromise.reject(TAG, "Received redirect uri but there is no source to process"); - createdSource = null; - createSourcePromise = null; + if (!mCreatedSource.getClientSecret().equals(clientSecret)) { + mCreateSourcePromise.reject(TAG, "Received redirect uri but there is no source to process"); + mCreatedSource = null; + mCreateSourcePromise = null; return; } final String sourceId = redirectData.getQueryParameter("source"); - if (!createdSource.getId().equals(sourceId)) { - createSourcePromise.reject(TAG, "Received wrong source id in redirect uri"); - createdSource = null; - createSourcePromise = null; + if (!mCreatedSource.getId().equals(sourceId)) { + mCreateSourcePromise.reject(TAG, "Received wrong source id in redirect uri"); + mCreatedSource = null; + mCreateSourcePromise = null; return; } - final Promise promise = createSourcePromise; + final Promise promise = mCreateSourcePromise; // Nulls those properties to avoid processing them twice - createdSource = null; - createSourcePromise = null; + mCreatedSource = null; + mCreateSourcePromise = null; new AsyncTask() { @Override protected Void doInBackground(Void... voids) { Source source = null; try { - source = stripe.retrieveSourceSynchronous(sourceId, clientSecret); + source = mStripe.retrieveSourceSynchronous(sourceId, clientSecret); } catch (Exception e) { - Log.w(TAG, "Failed to retrieve source", e); + return null; } @@ -447,515 +360,4 @@ protected Void doInBackground(Void... voids) { }.execute(); } - private void startApiClientAndAndroidPay(final Activity activity, final ReadableMap map) { - if (googleApiClient != null && googleApiClient.isConnected()) { - startAndroidPay(map); - } else { - googleApiClient = new GoogleApiClient.Builder(activity) - .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { - @Override - public void onConnected(@Nullable Bundle bundle) { - Log.d(TAG, "onConnected: "); - startAndroidPay(map); - } - - @Override - public void onConnectionSuspended(int i) { - Log.d(TAG, "onConnectionSuspended: "); - payPromise.reject(TAG, "onConnectionSuspended i = " + i); - } - }) - .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() { - @Override - public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { - Log.d(TAG, "onConnectionFailed: "); - payPromise.reject(TAG, "onConnectionFailed: " + connectionResult.getErrorMessage()); - } - }) - .addApi(Wallet.API, new Wallet.WalletOptions.Builder() - .setEnvironment(mEnvironment) - .setTheme(WalletConstants.THEME_LIGHT) - .build()) - .build(); - googleApiClient.connect(); - } - } - - private void showAndroidPay(final ReadableMap map) { - androidPayParams = map; - final String estimatedTotalPrice = map.getString(TOTAL_PRICE); - final String currencyCode = map.getString(CURRENCY_CODE); - final Boolean shippingAddressRequired = exist(map, SHIPPING_ADDRESS_REQUIRED, true); - final ArrayList allowedCountries = getAllowedShippingCountries(map); - final MaskedWalletRequest maskedWalletRequest = createWalletRequest(estimatedTotalPrice, currencyCode, shippingAddressRequired, allowedCountries); - Wallet.Payments.loadMaskedWallet(googleApiClient, maskedWalletRequest, LOAD_MASKED_WALLET_REQUEST_CODE); - } - - private MaskedWalletRequest createWalletRequest(final String estimatedTotalPrice, final String currencyCode, final Boolean shippingAddressRequired, final ArrayList countries) { - - final MaskedWalletRequest maskedWalletRequest = MaskedWalletRequest.newBuilder() - - // Request credit card tokenization with Stripe by specifying tokenization parameters: - .setPaymentMethodTokenizationParameters(PaymentMethodTokenizationParameters.newBuilder() - .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.PAYMENT_GATEWAY) - .addParameter("gateway", "stripe") - .addParameter("stripe:publishableKey", publicKey) - .addParameter("stripe:version", BuildConfig.VERSION_NAME) - .build()) - // You want the shipping address: - .setShippingAddressRequired(shippingAddressRequired) - .addAllowedCountrySpecificationsForShipping(countries) - // Price set as a decimal: - .setEstimatedTotalPrice(estimatedTotalPrice) - .setCurrencyCode(currencyCode) - .build(); - return maskedWalletRequest; - } - - private ArrayList getAllowedShippingCountries(final ReadableMap map) { - ArrayList allowedCountriesForShipping = new ArrayList<>(); - ReadableArray countries = exist(map, "shipping_countries", (ReadableArray) null); - - if(countries != null){ - for (int i = 0; i < countries.size(); i++) { - String code = countries.getString(i); - allowedCountriesForShipping.add(new CountrySpecification(code)); - } - } - - return allowedCountriesForShipping; - } - - private boolean isPlayServicesAvailable() { - GoogleApiAvailability googleAPI = GoogleApiAvailability.getInstance(); - int result = googleAPI.isGooglePlayServicesAvailable(getCurrentActivity()); - if (result != ConnectionResult.SUCCESS) { - return false; - } - return true; - } - - private void androidPayUnavaliableDialog() { - new AlertDialog.Builder(getCurrentActivity()) - .setMessage(R.string.gettipsi_android_pay_unavaliable) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - - private void handleLoadMascedWaletRequest(int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK) { - MaskedWallet maskedWallet = data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET); - - final Cart.Builder cartBuilder = Cart.newBuilder() - .setCurrencyCode(androidPayParams.getString(CURRENCY_CODE)) - .setTotalPrice(androidPayParams.getString(TOTAL_PRICE)); - - final ReadableArray lineItems = androidPayParams.getArray(LINE_ITEMS); - if (lineItems != null) { - for (int i = 0; i < lineItems.size(); i++) { - final ReadableMap lineItem = lineItems.getMap(i); - cartBuilder.addLineItem(LineItem.newBuilder() // Identify item being purchased - .setCurrencyCode(lineItem.getString(CURRENCY_CODE)) - .setQuantity(lineItem.getString(QUANTITY)) - .setDescription(DESCRIPTION) - .setTotalPrice(TOTAL_PRICE) - .setUnitPrice(UNIT_PRICE) - .build()); - } - } - - final FullWalletRequest fullWalletRequest = FullWalletRequest.newBuilder() - .setCart(cartBuilder.build()) - .setGoogleTransactionId(maskedWallet.getGoogleTransactionId()) - .build(); - - Wallet.Payments.loadFullWallet(googleApiClient, fullWalletRequest, LOAD_FULL_WALLET_REQUEST_CODE); - } else { - payPromise.reject(PURCHASE_CANCELLED, "Purchase was cancelled"); - } - } - - private IsReadyToPayRequest doIsReadyToPayRequest() { - return IsReadyToPayRequest.newBuilder().build(); - } - - private void checkAndroidPayAvaliable(final GoogleApiClient client, final Promise promise) { - Wallet.Payments.isReadyToPay(client, doIsReadyToPayRequest()).setResultCallback( - new ResultCallback() { - @Override - public void onResult(@NonNull BooleanResult booleanResult) { - if (booleanResult.getStatus().isSuccess()) { - promise.resolve(booleanResult.getValue()); - } else { - // Error making isReadyToPay call - Log.e(TAG, "isReadyToPay:" + booleanResult.getStatus()); - promise.reject(TAG, booleanResult.getStatus().getStatusMessage()); - } - } - }); - } - - private void startAndroidPay(final ReadableMap map) { - Wallet.Payments.isReadyToPay(googleApiClient, doIsReadyToPayRequest()).setResultCallback( - new ResultCallback() { - @Override - public void onResult(@NonNull BooleanResult booleanResult) { - Log.d(TAG, "onResult: "); - if (booleanResult.getStatus().isSuccess()) { - Log.d(TAG, "onResult: booleanResult.getStatus().isSuccess()"); - if (booleanResult.getValue()) { - // TODO Work only in few countries. I don't now how test it in our countries. - Log.d(TAG, "onResult: booleanResult.getValue()"); - showAndroidPay(map); - } else { - Log.d(TAG, "onResult: !booleanResult.getValue()"); - // Hide Android Pay buttons, show a message that Android Pay - // cannot be used yet, and display a traditional checkout button - androidPayUnavaliableDialog(); - payPromise.reject(TAG, "Android Pay unavaliable"); - } - } else { - // Error making isReadyToPay call - Log.e(TAG, "isReadyToPay:" + booleanResult.getStatus()); - androidPayUnavaliableDialog(); - payPromise.reject(TAG, "Error making isReadyToPay call"); - } - } - } - ); - } - - private Card createCard(final ReadableMap cardData) { - return new Card( - // required fields - cardData.getString("number"), - cardData.getInt("expMonth"), - cardData.getInt("expYear"), - // additional fields - exist(cardData, "cvc"), - exist(cardData, "name"), - exist(cardData, "addressLine1"), - exist(cardData, "addressLine2"), - exist(cardData, "addressCity"), - exist(cardData, "addressState"), - exist(cardData, "addressZip"), - exist(cardData, "addressCountry"), - exist(cardData, "brand"), - exist(cardData, "last4"), - exist(cardData, "fingerprint"), - exist(cardData, "funding"), - exist(cardData, "country"), - exist(cardData, "currency"), - exist(cardData, "id") - ); - } - - private WritableMap convertTokenToWritableMap(Token token) { - WritableMap newToken = Arguments.createMap(); - - if (token == null) return newToken; - - newToken.putString("tokenId", token.getId()); - newToken.putBoolean("livemode", token.getLivemode()); - newToken.putBoolean("used", token.getUsed()); - newToken.putDouble("created", token.getCreated().getTime()); - - if (token.getCard() != null) { - newToken.putMap("card", convertCardToWritableMap(token.getCard())); - } - if (token.getBankAccount() != null) { - newToken.putMap("bankAccount", convertBankAccountToWritableMap(token.getBankAccount())); - } - - return newToken; - } - - @NonNull - private WritableMap convertSourceToWritableMap(@Nullable Source source) { - WritableMap newSource = Arguments.createMap(); - - if (source == null) { - return newSource; - } - - newSource.putString("sourceId", source.getId()); - newSource.putInt("amount", source.getAmount().intValue()); - newSource.putInt("created", source.getCreated().intValue()); - newSource.putMap("codeVerification", convertCodeVerificationToWritableMap(source.getCodeVerification())); - newSource.putString("currency", source.getCurrency()); - newSource.putString("flow", source.getFlow()); - newSource.putBoolean("livemode", source.isLiveMode()); - newSource.putMap("metadata", stringMapToWritableMap(source.getMetaData())); - newSource.putMap("owner", convertOwnerToWritableMap(source.getOwner())); - newSource.putMap("receiver", convertReceiverToWritableMap(source.getReceiver())); - newSource.putMap("redirect", convertRedirectToWritableMap(source.getRedirect())); - newSource.putMap("sourceTypeData", mapToWritableMap(source.getSourceTypeData())); - newSource.putString("status", source.getStatus()); - newSource.putString("type", source.getType()); - newSource.putString("typeRaw", source.getTypeRaw()); - newSource.putString("usage", source.getUsage()); - - return newSource; - } - - @NonNull - private WritableMap stringMapToWritableMap(@Nullable Map map) { - WritableMap writableMap = Arguments.createMap(); - - if (map == null) { - return writableMap; - } - - for (Map.Entry entry : map.entrySet()) { - writableMap.putString(entry.getKey(), entry.getValue()); - } - - return writableMap; - } - - @NonNull - private WritableMap convertOwnerToWritableMap(@Nullable final SourceOwner owner) { - WritableMap map = Arguments.createMap(); - - if (owner == null) { - return map; - } - - map.putMap("address", convertAddressToWritableMap(owner.getAddress())); - map.putString("email", owner.getEmail()); - map.putString("name", owner.getName()); - map.putString("phone", owner.getPhone()); - map.putString("verifiedEmail", owner.getVerifiedEmail()); - map.putString("verifiedPhone", owner.getVerifiedPhone()); - map.putString("verifiedName", owner.getVerifiedName()); - map.putMap("verifiedAddress", convertAddressToWritableMap(owner.getVerifiedAddress())); - - return map; - } - - @NonNull - private WritableMap convertAddressToWritableMap(@Nullable final Address address) { - WritableMap map = Arguments.createMap(); - - if (address == null) { - return map; - } - - map.putString("city", address.getCity()); - map.putString("country", address.getCountry()); - map.putString("line1", address.getLine1()); - map.putString("line2", address.getLine2()); - map.putString("postalCode", address.getPostalCode()); - map.putString("state", address.getState()); - - return map; - } - - @NonNull - private WritableMap convertReceiverToWritableMap(@Nullable final SourceReceiver receiver) { - WritableMap map = Arguments.createMap(); - - if (receiver == null) { - return map; - } - - map.putInt("amountCharged", (int) receiver.getAmountCharged()); - map.putInt("amountReceived", (int) receiver.getAmountReceived()); - map.putInt("amountReturned", (int) receiver.getAmountReturned()); - map.putString("address", receiver.getAddress()); - - return map; - } - - @NonNull - private WritableMap convertRedirectToWritableMap(@Nullable SourceRedirect redirect) { - WritableMap map = Arguments.createMap(); - - if (redirect == null) { - return map; - } - - map.putString("returnUrl", redirect.getReturnUrl()); - map.putString("status", redirect.getStatus()); - map.putString("url", redirect.getUrl()); - - return map; - } - - @NonNull - private WritableMap convertCodeVerificationToWritableMap(@Nullable SourceCodeVerification codeVerification) { - WritableMap map = Arguments.createMap(); - - if (codeVerification == null) { - return map; - } - - map.putInt("attemptsRemaining", codeVerification.getAttemptsRemaining()); - map.putString("status", codeVerification.getStatus()); - - return map; - } - - @NonNull - private WritableMap mapToWritableMap(@Nullable Map map){ - WritableMap writableMap = Arguments.createMap(); - - if (map == null) { - return writableMap; - } - - for (String key: map.keySet()) { - pushRightTypeToMap(writableMap, key, map.get(key)); - } - - return writableMap; - } - - private void pushRightTypeToMap(@NonNull WritableMap map, @NonNull String key, @NonNull Object object) { - Class argumentClass = object.getClass(); - if (argumentClass == Boolean.class) { - map.putBoolean(key, (Boolean) object); - } else if (argumentClass == Integer.class) { - map.putDouble(key, ((Integer)object).doubleValue()); - } else if (argumentClass == Double.class) { - map.putDouble(key, (Double) object); - } else if (argumentClass == Float.class) { - map.putDouble(key, ((Float)object).doubleValue()); - } else if (argumentClass == String.class) { - map.putString(key, object.toString()); - } else if (argumentClass == WritableNativeMap.class) { - map.putMap(key, (WritableNativeMap)object); - } else if (argumentClass == WritableNativeArray.class) { - map.putArray(key, (WritableNativeArray) object); - } else { - Log.e(TAG, "Can't map "+ key + "value of " + argumentClass.getSimpleName() + " to any valid js type,"); - } - } - - private WritableMap convertCardToWritableMap(final Card card) { - WritableMap result = Arguments.createMap(); - - if(card == null) return result; - - result.putString("cardId", card.getId()); - result.putString("number", card.getNumber()); - result.putString("cvc", card.getCVC() ); - result.putInt("expMonth", card.getExpMonth() ); - result.putInt("expYear", card.getExpYear() ); - result.putString("name", card.getName() ); - result.putString("addressLine1", card.getAddressLine1() ); - result.putString("addressLine2", card.getAddressLine2() ); - result.putString("addressCity", card.getAddressCity() ); - result.putString("addressState", card.getAddressState() ); - result.putString("addressZip", card.getAddressZip() ); - result.putString("addressCountry", card.getAddressCountry() ); - result.putString("last4", card.getLast4() ); - result.putString("brand", card.getBrand() ); - result.putString("funding", card.getFunding() ); - result.putString("fingerprint", card.getFingerprint() ); - result.putString("country", card.getCountry() ); - result.putString("currency", card.getCurrency() ); - - return result; - } - - private WritableMap convertBankAccountToWritableMap(BankAccount account) { - WritableMap result = Arguments.createMap(); - - if(account == null) return result; - - result.putString("routingNumber", account.getRoutingNumber()); - result.putString("accountNumber", account.getAccountNumber()); - result.putString("countryCode", account.getCountryCode()); - result.putString("currency", account.getCurrency()); - result.putString("accountHolderName", account.getAccountHolderName()); - result.putString("accountHolderType", account.getAccountHolderType()); - result.putString("fingerprint", account.getFingerprint()); - result.putString("bankName", account.getBankName()); - result.putString("last4", account.getLast4()); - - return result; - } - - private WritableMap convertAddressToWritableMap(final UserAddress address){ - WritableMap result = Arguments.createMap(); - - if(address == null) return result; - - putIfExist(result, "address1", address.getAddress1()); - putIfExist(result, "address2", address.getAddress2()); - putIfExist(result, "address3", address.getAddress3()); - putIfExist(result, "address4", address.getAddress4()); - putIfExist(result, "address5", address.getAddress5()); - putIfExist(result, "administrativeArea", address.getAdministrativeArea()); - putIfExist(result, "companyName", address.getCompanyName()); - putIfExist(result, "countryCode", address.getCountryCode()); - putIfExist(result, "locality", address.getLocality()); - putIfExist(result, "name", address.getName()); - putIfExist(result, "phoneNumber", address.getPhoneNumber()); - putIfExist(result, "postalCode", address.getPostalCode()); - putIfExist(result, "sortingCode", address.getSortingCode()); - - return result; - } - - private BankAccount createBankAccount(ReadableMap accountData) { - BankAccount account = new BankAccount( - // required fields only - accountData.getString("accountNumber"), - accountData.getString("countryCode"), - accountData.getString("currency"), - exist(accountData, "routingNumber", "") - ); - account.setAccountHolderName(exist(accountData, "accountHolderName")); - account.setAccountHolderType(exist(accountData, "accountHolderType")); - - return account; - } - - private String exist(final ReadableMap map, final String key, final String def) { - if (map.hasKey(key)) { - return map.getString(key); - } else { - // If map don't have some key - we must pass to constructor default value. - return def; - } - } - - private void putIfExist(final WritableMap map, final String key, final String value) { - if (!TextUtils.isEmpty(value)) { - map.putString(key, value); - } - } - - private Boolean exist(final ReadableMap map, final String key, final Boolean def) { - if (map.hasKey(key)) { - return map.getBoolean(key); - } else { - // If map don't have some key - we must pass to constructor default value. - return def; - } - } - - private ReadableArray exist(final ReadableMap map, final String key, final ReadableArray def) { - if (map.hasKey(key)) { - return map.getArray(key); - } else { - // If map don't have some key - we must pass to constructor default value. - return def; - } - } - - private ReadableMap exist(final ReadableMap map, final String key, final ReadableMap def) { - if (map.hasKey(key)) { - return map.getMap(key); - } else { - // If map don't have some key - we must pass to constructor default value. - return def; - } - } - - private String exist(final ReadableMap map, final String key) { - return exist(map, key, (String) null); - } } diff --git a/android/src/main/java/com/gettipsi/stripe/dialog/AddCardDialogFragment.java b/android/src/main/java/com/gettipsi/stripe/dialog/AddCardDialogFragment.java index 8de9df0b3..b30407c04 100644 --- a/android/src/main/java/com/gettipsi/stripe/dialog/AddCardDialogFragment.java +++ b/android/src/main/java/com/gettipsi/stripe/dialog/AddCardDialogFragment.java @@ -222,7 +222,7 @@ public void onError(Exception error) { public void showToast(String message) { Context context = getActivity(); - if(context != null && !TextUtils.isEmpty(message)) { + if (context != null && !TextUtils.isEmpty(message)) { Toast.makeText(context, message, Toast.LENGTH_LONG).show(); } } diff --git a/android/src/main/java/com/gettipsi/stripe/util/Action.java b/android/src/main/java/com/gettipsi/stripe/util/Action.java new file mode 100644 index 000000000..6b142ebdf --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/util/Action.java @@ -0,0 +1,5 @@ +package com.gettipsi.stripe.util; + +public interface Action { + void call(T t); +} diff --git a/android/src/main/java/com/gettipsi/stripe/util/ArgCheck.java b/android/src/main/java/com/gettipsi/stripe/util/ArgCheck.java new file mode 100644 index 000000000..f94ac365d --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/util/ArgCheck.java @@ -0,0 +1,35 @@ +package com.gettipsi.stripe.util; + +import android.text.TextUtils; + +public abstract class ArgCheck { + + public static T nonNull(T t) { + if (t == null) { + throw new NullPointerException(); + } + + return t; + } + + public static String notEmptyString(String string) { + if (TextUtils.isEmpty(string)) { + throw new IllegalArgumentException(); + } + + return string; + } + + public static String isDouble(String string) { + String digital = notEmptyString(string); + Double.parseDouble(string); + return digital; + } + + public static void isTrue(boolean shouldBeTrue) { + if (!shouldBeTrue) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/android/src/main/java/com/gettipsi/stripe/util/Converters.java b/android/src/main/java/com/gettipsi/stripe/util/Converters.java new file mode 100644 index 000000000..d22e10e78 --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/util/Converters.java @@ -0,0 +1,415 @@ +package com.gettipsi.stripe.util; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.google.android.gms.identity.intents.model.CountrySpecification; +import com.google.android.gms.identity.intents.model.UserAddress; +import com.google.android.gms.wallet.PaymentData; +import com.stripe.android.model.Address; +import com.stripe.android.model.BankAccount; +import com.stripe.android.model.Card; +import com.stripe.android.model.Source; +import com.stripe.android.model.SourceCodeVerification; +import com.stripe.android.model.SourceOwner; +import com.stripe.android.model.SourceReceiver; +import com.stripe.android.model.SourceRedirect; +import com.stripe.android.model.Token; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +/** + * Created by ngoriachev on 13/03/2018. + */ + +public class Converters { + + public static WritableMap convertTokenToWritableMap(Token token) { + WritableMap newToken = Arguments.createMap(); + + if (token == null) return newToken; + + newToken.putString("tokenId", token.getId()); + newToken.putBoolean("livemode", token.getLivemode()); + newToken.putBoolean("used", token.getUsed()); + newToken.putDouble("created", token.getCreated().getTime()); + + if (token.getCard() != null) { + newToken.putMap("card", convertCardToWritableMap(token.getCard())); + } + if (token.getBankAccount() != null) { + newToken.putMap("bankAccount", convertBankAccountToWritableMap(token.getBankAccount())); + } + + return newToken; + } + + public static WritableMap putExtraToTokenMap(final WritableMap tokenMap, UserAddress billingAddress, UserAddress shippingAddress) { + ArgCheck.nonNull(tokenMap); + + WritableMap extra = Arguments.createMap(); + + extra.putMap("billingContact", convertAddressToWritableMap(billingAddress)); + extra.putMap("shippingContact", convertAddressToWritableMap(shippingAddress)); + tokenMap.putMap("extra", extra); + + return tokenMap; + } + + private static WritableMap convertCardToWritableMap(final Card card) { + WritableMap result = Arguments.createMap(); + + if (card == null) return result; + + result.putString("cardId", card.getId()); + result.putString("number", card.getNumber()); + result.putString("cvc", card.getCVC() ); + result.putInt("expMonth", card.getExpMonth() ); + result.putInt("expYear", card.getExpYear() ); + result.putString("name", card.getName() ); + result.putString("addressLine1", card.getAddressLine1() ); + result.putString("addressLine2", card.getAddressLine2() ); + result.putString("addressCity", card.getAddressCity() ); + result.putString("addressState", card.getAddressState() ); + result.putString("addressZip", card.getAddressZip() ); + result.putString("addressCountry", card.getAddressCountry() ); + result.putString("last4", card.getLast4() ); + result.putString("brand", card.getBrand() ); + result.putString("funding", card.getFunding() ); + result.putString("fingerprint", card.getFingerprint() ); + result.putString("country", card.getCountry() ); + result.putString("currency", card.getCurrency() ); + + return result; + } + + public static WritableMap convertBankAccountToWritableMap(BankAccount account) { + WritableMap result = Arguments.createMap(); + + if (account == null) return result; + + result.putString("routingNumber", account.getRoutingNumber()); + result.putString("accountNumber", account.getAccountNumber()); + result.putString("countryCode", account.getCountryCode()); + result.putString("currency", account.getCurrency()); + result.putString("accountHolderName", account.getAccountHolderName()); + result.putString("accountHolderType", account.getAccountHolderType()); + result.putString("fingerprint", account.getFingerprint()); + result.putString("bankName", account.getBankName()); + result.putString("last4", account.getLast4()); + + return result; + } + + public static String getValue(final ReadableMap map, final String key, final String def) { + if (map.hasKey(key)) { + return map.getString(key); + } else { + // If map don't have some key - we must pass to constructor default value. + return def; + } + } + + public static Boolean getValue(final ReadableMap map, final String key, final Boolean def) { + if (map.hasKey(key)) { + return map.getBoolean(key); + } else { + // If map don't have some key - we must pass to constructor default value. + return def; + } + } + + public static ReadableArray getValue(final ReadableMap map, final String key, final ReadableArray def) { + if (map.hasKey(key)) { + return map.getArray(key); + } else { + // If map don't have some key - we must pass to constructor default value. + return def; + } + } + + public static String getValue(final ReadableMap map, final String key) { + return getValue(map, key, (String) null); + } + + public static Collection getAllowedShippingCountryCodes(final ReadableMap map) { + ArrayList allowedCountryCodesForShipping = new ArrayList<>(); + ReadableArray countries = getValue(map, "shipping_countries", (ReadableArray) null); + + if (countries != null){ + for (int i = 0; i < countries.size(); i++) { + String code = countries.getString(i); + allowedCountryCodesForShipping.add(code); + } + } + + return allowedCountryCodesForShipping; + } + + public static ArrayList getAllowedShippingCountries(final ReadableMap map) { + ArrayList allowedCountriesForShipping = new ArrayList<>(); + ReadableArray countries = getValue(map, "shipping_countries", (ReadableArray) null); + + if (countries != null){ + for (int i = 0; i < countries.size(); i++) { + String code = countries.getString(i); + allowedCountriesForShipping.add(new CountrySpecification(code)); + } + } + + return allowedCountriesForShipping; + } + + public static Card createCard(final ReadableMap cardData) { + return new Card( + // required fields + cardData.getString("number"), + cardData.getInt("expMonth"), + cardData.getInt("expYear"), + // additional fields + getValue(cardData, "cvc"), + getValue(cardData, "name"), + getValue(cardData, "addressLine1"), + getValue(cardData, "addressLine2"), + getValue(cardData, "addressCity"), + getValue(cardData, "addressState"), + getValue(cardData, "addressZip"), + getValue(cardData, "addressCountry"), + getValue(cardData, "brand"), + getValue(cardData, "last4"), + getValue(cardData, "fingerprint"), + getValue(cardData, "funding"), + getValue(cardData, "country"), + getValue(cardData, "currency"), + getValue(cardData, "id") + ); + } + + + + @NonNull + public static WritableMap convertSourceToWritableMap(@Nullable Source source) { + WritableMap newSource = Arguments.createMap(); + + if (source == null) { + return newSource; + } + + newSource.putString("sourceId", source.getId()); + newSource.putInt("amount", source.getAmount().intValue()); + newSource.putInt("created", source.getCreated().intValue()); + newSource.putMap("codeVerification", convertCodeVerificationToWritableMap(source.getCodeVerification())); + newSource.putString("currency", source.getCurrency()); + newSource.putString("flow", source.getFlow()); + newSource.putBoolean("livemode", source.isLiveMode()); + newSource.putMap("metadata", stringMapToWritableMap(source.getMetaData())); + newSource.putMap("owner", convertOwnerToWritableMap(source.getOwner())); + newSource.putMap("receiver", convertReceiverToWritableMap(source.getReceiver())); + newSource.putMap("redirect", convertRedirectToWritableMap(source.getRedirect())); + newSource.putMap("sourceTypeData", mapToWritableMap(source.getSourceTypeData())); + newSource.putString("status", source.getStatus()); + newSource.putString("type", source.getType()); + newSource.putString("typeRaw", source.getTypeRaw()); + newSource.putString("usage", source.getUsage()); + + return newSource; + } + + @NonNull + public static WritableMap stringMapToWritableMap(@Nullable Map map) { + WritableMap writableMap = Arguments.createMap(); + + if (map == null) { + return writableMap; + } + + for (Map.Entry entry : map.entrySet()) { + writableMap.putString(entry.getKey(), entry.getValue()); + } + + return writableMap; + } + + @NonNull + public static WritableMap convertOwnerToWritableMap(@Nullable final SourceOwner owner) { + WritableMap map = Arguments.createMap(); + + if (owner == null) { + return map; + } + + map.putMap("address", convertAddressToWritableMap(owner.getAddress())); + map.putString("email", owner.getEmail()); + map.putString("name", owner.getName()); + map.putString("phone", owner.getPhone()); + map.putString("verifiedEmail", owner.getVerifiedEmail()); + map.putString("verifiedPhone", owner.getVerifiedPhone()); + map.putString("verifiedName", owner.getVerifiedName()); + map.putMap("verifiedAddress", convertAddressToWritableMap(owner.getVerifiedAddress())); + + return map; + } + + @NonNull + public static WritableMap convertAddressToWritableMap(@Nullable final Address address) { + WritableMap map = Arguments.createMap(); + + if (address == null) { + return map; + } + + map.putString("city", address.getCity()); + map.putString("country", address.getCountry()); + map.putString("line1", address.getLine1()); + map.putString("line2", address.getLine2()); + map.putString("postalCode", address.getPostalCode()); + map.putString("state", address.getState()); + + return map; + } + + @NonNull + public static WritableMap convertReceiverToWritableMap(@Nullable final SourceReceiver receiver) { + WritableMap map = Arguments.createMap(); + + if (receiver == null) { + return map; + } + + map.putInt("amountCharged", (int) receiver.getAmountCharged()); + map.putInt("amountReceived", (int) receiver.getAmountReceived()); + map.putInt("amountReturned", (int) receiver.getAmountReturned()); + map.putString("address", receiver.getAddress()); + + return map; + } + + @NonNull + public static WritableMap convertRedirectToWritableMap(@Nullable SourceRedirect redirect) { + WritableMap map = Arguments.createMap(); + + if (redirect == null) { + return map; + } + + map.putString("returnUrl", redirect.getReturnUrl()); + map.putString("status", redirect.getStatus()); + map.putString("url", redirect.getUrl()); + + return map; + } + + @NonNull + public static WritableMap convertCodeVerificationToWritableMap(@Nullable SourceCodeVerification codeVerification) { + WritableMap map = Arguments.createMap(); + + if (codeVerification == null) { + return map; + } + + map.putInt("attemptsRemaining", codeVerification.getAttemptsRemaining()); + map.putString("status", codeVerification.getStatus()); + + return map; + } + + @NonNull + public static WritableMap mapToWritableMap(@Nullable Map map){ + WritableMap writableMap = Arguments.createMap(); + + if (map == null) { + return writableMap; + } + + for (String key: map.keySet()) { + pushRightTypeToMap(writableMap, key, map.get(key)); + } + + return writableMap; + } + + public static void pushRightTypeToMap(@NonNull WritableMap map, @NonNull String key, @NonNull Object object) { + Class argumentClass = object.getClass(); + if (argumentClass == Boolean.class) { + map.putBoolean(key, (Boolean) object); + } else if (argumentClass == Integer.class) { + map.putDouble(key, ((Integer)object).doubleValue()); + } else if (argumentClass == Double.class) { + map.putDouble(key, (Double) object); + } else if (argumentClass == Float.class) { + map.putDouble(key, ((Float)object).doubleValue()); + } else if (argumentClass == String.class) { + map.putString(key, object.toString()); + } else if (argumentClass == WritableNativeMap.class) { + map.putMap(key, (WritableNativeMap)object); + } else if (argumentClass == WritableNativeArray.class) { + map.putArray(key, (WritableNativeArray) object); + } else { + + } + } + + public static WritableMap convertAddressToWritableMap(final UserAddress address){ + WritableMap result = Arguments.createMap(); + + if (address == null) return result; + + putIfNotEmpty(result, "address1", address.getAddress1()); + putIfNotEmpty(result, "address2", address.getAddress2()); + putIfNotEmpty(result, "address3", address.getAddress3()); + putIfNotEmpty(result, "address4", address.getAddress4()); + putIfNotEmpty(result, "address5", address.getAddress5()); + putIfNotEmpty(result, "administrativeArea", address.getAdministrativeArea()); + putIfNotEmpty(result, "companyName", address.getCompanyName()); + putIfNotEmpty(result, "countryCode", address.getCountryCode()); + putIfNotEmpty(result, "locality", address.getLocality()); + putIfNotEmpty(result, "name", address.getName()); + putIfNotEmpty(result, "phoneNumber", address.getPhoneNumber()); + putIfNotEmpty(result, "postalCode", address.getPostalCode()); + putIfNotEmpty(result, "sortingCode", address.getSortingCode()); + + return result; + } + + public static BankAccount createBankAccount(ReadableMap accountData) { + BankAccount account = new BankAccount( + // required fields only + accountData.getString("accountNumber"), + accountData.getString("countryCode"), + accountData.getString("currency"), + getValue(accountData, "routingNumber", "") + ); + account.setAccountHolderName(getValue(accountData, "accountHolderName")); + account.setAccountHolderType(getValue(accountData, "accountHolderType")); + + return account; + } + + public static String getStringOrNull(@NonNull ReadableMap map, @NonNull String key) { + return map.hasKey(key) ? map.getString(key) : null; + } + + public static void putIfNotEmpty(final WritableMap map, final String key, final String value) { + if (!TextUtils.isEmpty(value)) { + map.putString(key, value); + } + } + + public static UserAddress getBillingAddress(PaymentData paymentData) { + if (paymentData != null && paymentData.getCardInfo() != null) { + return paymentData.getCardInfo().getBillingAddress(); + } + + return null; + } + +} diff --git a/android/src/main/java/com/gettipsi/stripe/util/Fun0.java b/android/src/main/java/com/gettipsi/stripe/util/Fun0.java new file mode 100644 index 000000000..78ca74f9d --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/util/Fun0.java @@ -0,0 +1,5 @@ +package com.gettipsi.stripe.util; + +public interface Fun0 { + R call(); +} diff --git a/android/src/main/java/com/gettipsi/stripe/util/InitializationOptions.java b/android/src/main/java/com/gettipsi/stripe/util/InitializationOptions.java new file mode 100644 index 000000000..26e2fa663 --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/util/InitializationOptions.java @@ -0,0 +1,14 @@ +package com.gettipsi.stripe.util; + +/** + * Created by ngoriachev on 15/03/2018. + */ + +public abstract class InitializationOptions { + + public static final String PUBLISHABLE_KEY = "publishableKey"; + public static final String ANDROID_PAY_MODE_KEY = "androidPayMode"; + public static final String ANDROID_PAY_MODE_PRODUCTION = "production"; + public static final String ANDROID_PAY_MODE_TEST = "test"; + +} diff --git a/android/src/main/java/com/gettipsi/stripe/util/PayParams.java b/android/src/main/java/com/gettipsi/stripe/util/PayParams.java new file mode 100644 index 000000000..acaec1497 --- /dev/null +++ b/android/src/main/java/com/gettipsi/stripe/util/PayParams.java @@ -0,0 +1,18 @@ +package com.gettipsi.stripe.util; + +/** + * Created by ngoriachev on 13/03/2018. + */ + +public abstract class PayParams { + + public static final String CURRENCY_CODE = "currency_code"; + public static final String BILLING_ADDRESS_REQUIRED = "billing_address_required"; + public static final String SHIPPING_ADDRESS_REQUIRED = "shipping_address_required"; + public static final String TOTAL_PRICE = "total_price"; + public static final String UNIT_PRICE = "unit_price"; + public static final String LINE_ITEMS = "line_items"; + public static final String QUANTITY = "quantity"; + public static final String DESCRIPTION = "description"; + +} diff --git a/example/src/Root.js b/example/src/Root.js index dcc94ffce..fc277e3a7 100644 --- a/example/src/Root.js +++ b/example/src/Root.js @@ -13,7 +13,7 @@ import CardTextFieldScreen from './scenes/CardTextFieldScreen' import SourceScreen from './scenes/SourceScreen' import testID from './utils/testID' -stripe.init({ +stripe.setOptions({ publishableKey: '', merchantId: '', androidPayMode: 'test', diff --git a/package.json b/package.json index cbea8caa4..34dbe9ba0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tipsi-stripe", - "version": "4.6.4", + "version": "5.0.0", "description": "React Native Stripe binding for iOS/Andriod platforms", "main": "src/index.js", "scripts": { diff --git a/src/Stripe.android.js b/src/Stripe.android.js index 3631e55f5..6d47dd247 100644 --- a/src/Stripe.android.js +++ b/src/Stripe.android.js @@ -8,10 +8,10 @@ const { StripeModule } = NativeModules class Stripe { stripeInitialized = false - init = (options = {}) => { + setOptions = (options = {}) => { checkArgs( - types.initOptionsPropTypes, - options, 'options', 'Stripe.init' + types.setOptionsOptionsPropTypes, + options, 'options', 'Stripe.setOptions' ) this.stripeInitialized = true return StripeModule.init(options) diff --git a/src/Stripe.ios.js b/src/Stripe.ios.js index 605f899be..7e8f9feb2 100644 --- a/src/Stripe.ios.js +++ b/src/Stripe.ios.js @@ -22,10 +22,10 @@ class Stripe { } } - init = (options = {}) => { + setOptions = (options = {}) => { checkArgs( - types.initOptionsPropTypes, - options, 'options', 'Stripe.init' + types.setOptionsOptionsPropTypes, + options, 'options', 'Stripe.setOptions' ) this.stripeInitialized = true return TPSStripeManager.init(options) diff --git a/src/utils/types.js b/src/utils/types.js index b362adbe1..db517563e 100644 --- a/src/utils/types.js +++ b/src/utils/types.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types' const availableApplePayNetworks = ['american_express', 'discover', 'master_card', 'visa'] const availableApplePayAddressFields = ['all', 'name', 'email', 'phone', 'postal_address'] -export const initOptionsPropTypes = { - publishableKey: PropTypes.string.isRequired, +export const setOptionsOptionsPropTypes = { + publishableKey: PropTypes.string, merchantId: PropTypes.string, androidPayMode: PropTypes.string, }