diff --git a/.gitignore b/.gitignore index 31f01ae042..a8f28e7bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /build /captures .idea +api_configuration.xml \ No newline at end of file diff --git a/README.md b/README.md index 8cd6421759..ce6e1d3ac7 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ This README provides the usage manual for the SDK itself. For the full documenta To integrate the Adyen SDK into your project, import the **core** or **ui** module by adding one of the following lines to your build.gradle file. ``` -compile 'com.adyen.checkout:core:1.0.6' -compile 'com.adyen.checkout:ui:1.0.6' +compile 'com.adyen.checkout:core:1.4.1' +compile 'com.adyen.checkout:ui:1.4.1' ``` To give you as much flexibility as possible, our Android SDK can be integrated in two ways: @@ -137,9 +137,29 @@ This method is called only if payment details are required. For example, if Cred ```java @Override -public void onPaymentDetailsRequired(@NonNull final PaymentRequest paymentRequest, @NonNull final Map requiredFields, @NonNull final PaymentDetailsCallback callback) { - // For different payment methods different UI might be required. It is suggested to check selected method via paymentRequest.getPaymentMethod() and display UI accordingly. - // When all payment details are retrieved, SDK should be notified via PaymentDetailsCallback. +public void onPaymentDetailsRequired(@NonNull final PaymentRequest paymentRequest, @NonNull final Collection inputDetails, @NonNull final PaymentDetailsCallback callback) { + // For different payment methods different UI might be required. It is suggested to check selected method via paymentRequest.getPaymentMethod() and display UI accordingly. + // When all payment details are retrieved, SDK should be notified via PaymentDetailsCallback. + if (PaymentMethod.Type.CARD.equals(paymentRequest.getPaymentMethod().getType())) { + Card card = new Card(); + card.setNumber("4111111111111111"); + card.setCardHolderName("checkout shopper"); + card.setCvc("737"); + card.setExpiryMonth("10"); + card.setExpiryYear("2020"); + card.setGenerationTime(new Date()); + + try { + //Create PaymentDetails object from the inputDetails and fill them with the shopper input. + //Then call callback.completionWithPaymentDetails. + CreditCardPaymentDetails creditCardPaymentDetails = new CreditCardPaymentDetails(inputDetails); + creditCardPaymentDetails.fillCardToken(card.serialize(paymentRequest.getPublicKey())); + creditCardPaymentDetails.fillStoreDetails(true); + callback.completionWithPaymentDetails(creditCardPaymentDetails); + } catch (EncrypterException e) { + e.printStackTrace(); + } + } } ``` diff --git a/adyen-core/build.gradle b/adyen-core/build.gradle index 978078ef3e..45cd6d56d8 100644 --- a/adyen-core/build.gradle +++ b/adyen-core/build.gradle @@ -49,8 +49,8 @@ dependencies { compile "com.android.support:support-annotations:${rootProject.supportAnnotationsVersion}" compile 'com.android.support:customtabs:25.3.1' - compile 'io.reactivex:rxjava:1.1.6' - compile 'io.reactivex:rxandroid:1.2.1' + compile 'io.reactivex.rxjava2:rxjava:2.1.2' + compile 'io.reactivex.rxjava2:rxandroid:2.0.1' compile 'com.adyen.cse:adyen-cse:1.0.2' } diff --git a/adyen-core/src/main/java/com/adyen/core/DeviceTokenGenerator.java b/adyen-core/src/main/java/com/adyen/core/DeviceTokenGenerator.java index 639ae66008..bdb81545d5 100644 --- a/adyen-core/src/main/java/com/adyen/core/DeviceTokenGenerator.java +++ b/adyen-core/src/main/java/com/adyen/core/DeviceTokenGenerator.java @@ -38,7 +38,7 @@ static String getToken(final Context context, final PaymentStateHandler paymentS Settings.Secure.ANDROID_ID); deviceInfo.put("deviceFingerprintVersion", DEVICE_FINGER_PRINT_VERSION); deviceInfo.put("platform", "android"); - deviceInfo.put("apiVersion", "2"); + deviceInfo.put("apiVersion", "3"); deviceInfo.put("osVersion", Build.VERSION.SDK_INT); deviceInfo.put("sdkVersion", SDK_VERSION); deviceInfo.put("deviceIdentifier", androidId); diff --git a/adyen-core/src/main/java/com/adyen/core/PaymentStateHandler.java b/adyen-core/src/main/java/com/adyen/core/PaymentStateHandler.java index f204a55653..1efd0e82e5 100644 --- a/adyen-core/src/main/java/com/adyen/core/PaymentStateHandler.java +++ b/adyen-core/src/main/java/com/adyen/core/PaymentStateHandler.java @@ -41,8 +41,8 @@ import java.util.List; import java.util.Map; -import rx.Observable; -import rx.functions.Action1; +import io.reactivex.Observable; +import io.reactivex.functions.Consumer; import static com.adyen.core.constants.Constants.PaymentRequest.ADYEN_UI_FINALIZE_INTENT; import static com.adyen.core.constants.Constants.PaymentRequest.PAYMENT_DETAILS_PROVIDED_INTENT; @@ -257,9 +257,9 @@ private void fetchPaymentMethods() { this.preferredPaymentMethods = paymentResponse.getPreferredPaymentMethods(); Observable> listObservable = ModuleAvailabilityUtil.filterPaymentMethods(context, unfilteredPaymentMethods); - listObservable.subscribe(new Action1>() { + listObservable.subscribe(new Consumer>() { @Override - public void call(List filteredPaymentMethods) { + public void accept(List filteredPaymentMethods) { filteredPaymentMethods.removeAll(Collections.singleton(null)); PaymentStateHandler.this.filteredPaymentMethodsList.clear(); PaymentStateHandler.this.filteredPaymentMethodsList.addAll(filteredPaymentMethods); @@ -525,7 +525,15 @@ public void onFailure(Throwable e) { private static JSONObject paymentDetailsToJson(@NonNull PaymentDetails paymentDetails) throws JSONException { JSONObject jsonObject = new JSONObject(); for (InputDetail inputDetail : paymentDetails.getInputDetails()) { - jsonObject.put(inputDetail.getKey(), inputDetail.getValue()); + if (inputDetail.getInputDetails() != null && !inputDetail.getInputDetails().isEmpty()) { + JSONObject recursiveDetailJson = new JSONObject(); + for (InputDetail recursiveDetail : inputDetail.getInputDetails()) { + recursiveDetailJson.put(recursiveDetail.getKey(), recursiveDetail.getValue()); + } + jsonObject.put(inputDetail.getKey(), recursiveDetailJson); + } else { + jsonObject.put(inputDetail.getKey(), inputDetail.getValue()); + } } return jsonObject; } diff --git a/adyen-core/src/main/java/com/adyen/core/internals/ModuleAvailabilityUtil.java b/adyen-core/src/main/java/com/adyen/core/internals/ModuleAvailabilityUtil.java index e3eb4388ed..3d31ac89a4 100644 --- a/adyen-core/src/main/java/com/adyen/core/internals/ModuleAvailabilityUtil.java +++ b/adyen-core/src/main/java/com/adyen/core/internals/ModuleAvailabilityUtil.java @@ -11,11 +11,12 @@ import java.util.List; -import rx.Observable; -import rx.Subscriber; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func1; -import rx.schedulers.Schedulers; +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.functions.Function; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.android.schedulers.AndroidSchedulers; /** * @@ -48,29 +49,26 @@ public static PaymentMethodService getModulePaymentService(@NonNull PaymentModul public static Observable> filterPaymentMethods( @NonNull final Context context, @NonNull List unfilteredPaymentMethods) { - return Observable.from(unfilteredPaymentMethods).concatMap( - new Func1>() { + return Observable.fromIterable(unfilteredPaymentMethods).concatMap( + new Function>() { @Override - public Observable call(final PaymentMethod paymentMethod) { - return Observable.create(new Observable.OnSubscribe() { + public Observable apply(final PaymentMethod paymentMethod) { + return Observable.create(new ObservableOnSubscribe() { @Override - public void call(final Subscriber subscriber) { + public void subscribe(final ObservableEmitter subscriber) { isPaymentMethodAvailable(context, paymentMethod, new PaymentMethodAvailabilityCallback() { @Override public void onSuccess(boolean isAvailable) { - if (!subscriber.isUnsubscribed() && isAvailable) { + if (!subscriber.isDisposed() && isAvailable) { subscriber.onNext(paymentMethod); - } else { - subscriber.onNext(null); } - subscriber.onCompleted(); + subscriber.onComplete(); } @Override public void onFail(Throwable e) { - subscriber.onNext(null); - subscriber.onCompleted(); + subscriber.onComplete(); } }); } @@ -78,6 +76,7 @@ public void onFail(Throwable e) { } }) .toList() + .toObservable() .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()); } diff --git a/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/CreditCardPaymentDetails.java b/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/CreditCardPaymentDetails.java index fc5704e59a..4f7149b964 100644 --- a/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/CreditCardPaymentDetails.java +++ b/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/CreditCardPaymentDetails.java @@ -10,6 +10,17 @@ public class CreditCardPaymentDetails extends PaymentDetails { public static final String ADDITIONAL_DATA_CARD = "additionalData.card.encrypted.json"; public static final String STORE_DETAILS = "storeDetails"; + public static final String INSTALLMENTS = "installments"; + public static final String BILLING_ADDRESS = "billingAddress"; + + public enum AddressKey { + street, + houseNumberOrName, + city, + country, + postalCode, + stateOrProvince; + } public CreditCardPaymentDetails(Collection inputDetails) { super(inputDetails); @@ -23,4 +34,86 @@ public boolean fillStoreDetails(final boolean storeDetails) { return super.fill(STORE_DETAILS, storeDetails); } + public boolean fillNumberOfInstallments(final short numberOfInstallments) { + return super.fill(INSTALLMENTS, String.valueOf(numberOfInstallments)); + } + + public boolean fillbillingAddressStreet(String street) { + for (InputDetail inputDetail : getInputDetails()) { + if (inputDetail.getKey().equals(BILLING_ADDRESS)) { + for (InputDetail addressDetail : inputDetail.getInputDetails()) { + if (addressDetail.getKey().equals(AddressKey.street.name())) { + return addressDetail.fill(street); + } + } + } + } + return false; + } + + public boolean fillbillingAddressHouseNumberOrName(String houseNumberOrName) { + for (InputDetail inputDetail : getInputDetails()) { + if (inputDetail.getKey().equals(BILLING_ADDRESS)) { + for (InputDetail addressDetail : inputDetail.getInputDetails()) { + if (addressDetail.getKey().equals(AddressKey.houseNumberOrName.name())) { + return addressDetail.fill(houseNumberOrName); + } + } + } + } + return false; + } + + public boolean fillbillingAddressCity(String city) { + for (InputDetail inputDetail : getInputDetails()) { + if (inputDetail.getKey().equals(BILLING_ADDRESS)) { + for (InputDetail addressDetail : inputDetail.getInputDetails()) { + if (addressDetail.getKey().equals(AddressKey.city.name())) { + return addressDetail.fill(city); + } + } + } + } + return false; + } + + public boolean fillbillingAddressCountry(String country) { + for (InputDetail inputDetail : getInputDetails()) { + if (inputDetail.getKey().equals(BILLING_ADDRESS)) { + for (InputDetail addressDetail : inputDetail.getInputDetails()) { + if (addressDetail.getKey().equals(AddressKey.country.name())) { + return addressDetail.fill(country); + } + } + } + } + return false; + } + + public boolean fillbillingAddressPostalCode(String postalCode) { + for (InputDetail inputDetail : getInputDetails()) { + if (inputDetail.getKey().equals(BILLING_ADDRESS)) { + for (InputDetail addressDetail : inputDetail.getInputDetails()) { + if (addressDetail.getKey().equals(AddressKey.postalCode.name())) { + return addressDetail.fill(postalCode); + } + } + } + } + return false; + } + + public boolean fillbillingAddressStateOrProvince(String stateOrProvince) { + for (InputDetail inputDetail : getInputDetails()) { + if (inputDetail.getKey().equals(BILLING_ADDRESS)) { + for (InputDetail addressDetail : inputDetail.getInputDetails()) { + if (addressDetail.getKey().equals(AddressKey.stateOrProvince.name())) { + return addressDetail.fill(stateOrProvince); + } + } + } + } + return false; + } + } diff --git a/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/InputDetail.java b/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/InputDetail.java index 2902fe1471..b395504711 100644 --- a/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/InputDetail.java +++ b/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/InputDetail.java @@ -20,6 +20,8 @@ public final class InputDetail implements Serializable { private boolean optional = true; private ArrayList items = new ArrayList<>(); + private ArrayList inputDetails; + private InputDetail() { } @@ -39,7 +41,16 @@ public boolean fill(boolean value) { } public boolean isFilled() { - return !StringUtils.isEmptyOrNull(value); + if (inputDetails != null && !inputDetails.isEmpty()) { + for (InputDetail inputDetail : inputDetails) { + if (!inputDetail.isFilled()) { + return false; + } + } + return true; + } else { + return !StringUtils.isEmptyOrNull(value); + } } public static InputDetail fromJson(@NonNull final JSONObject jsonObject) throws JSONException { @@ -47,15 +58,32 @@ public static InputDetail fromJson(@NonNull final JSONObject jsonObject) throws inputDetail.key = jsonObject.getString("key"); inputDetail.optional = jsonObject.optBoolean("optional", false); inputDetail.type = Type.fromString(jsonObject.getString("type")); + inputDetail.value = jsonObject.optString("value"); if (inputDetail.type == Type.Select) { JSONArray jsonItems = jsonObject.getJSONArray("items"); for (int i = 0; i < jsonItems.length(); i++) { inputDetail.items.add(Item.fromJson(jsonItems.getJSONObject(i))); } } + if (jsonObject.has("inputDetails")) { + JSONArray jsonObjectInputDetails = jsonObject.getJSONArray("inputDetails"); + for (int i = 0; i < jsonObjectInputDetails.length(); i++) { + inputDetail.addInputDetail(fromJson(jsonObjectInputDetails.getJSONObject(i))); + } + } return inputDetail; } + private void addInputDetail(InputDetail inputDetail) { + if (inputDetails == null) { + this.inputDetails = new ArrayList<>(); + } + inputDetails.add(inputDetail); + } + + public ArrayList getInputDetails() { + return inputDetails; + } public String getKey() { return key; @@ -87,7 +115,7 @@ public enum Type implements Serializable { AndroidPayToken("androidPayToken"), // A token used by a wallet SamsungPayToken("samsungPayToken"), // A token used by a wallet Cvc("cvc"), //A field to enter CVC code - Address("Address"), + Address("address"), Unknown("Unknown"); private String apiField; @@ -123,7 +151,7 @@ private Item() { static Item fromJson(@NonNull final JSONObject jsonObject) throws JSONException { Item item = new Item(); item.id = jsonObject.getString("id"); - item.imageUrl = jsonObject.getString("imageUrl"); + item.imageUrl = jsonObject.optString("imageUrl"); item.name = jsonObject.getString("name"); return item; } diff --git a/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/PaymentDetails.java b/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/PaymentDetails.java index f6f1a66582..f4cc14c312 100644 --- a/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/PaymentDetails.java +++ b/adyen-core/src/main/java/com/adyen/core/models/paymentdetails/PaymentDetails.java @@ -4,6 +4,9 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.adyen.core.PaymentRequest; +import com.adyen.core.interfaces.PaymentDetailsCallback; + import java.io.Serializable; import java.util.Collection; import java.util.HashMap; @@ -11,11 +14,18 @@ /** * Class for collecting the required PaymentDetails from the shopper to make the payment. + * This is the generic {@Link PaymentDetails} class that can handle all payment methods. + * For convenience, several more specific subclasses of this class are provided. */ public class PaymentDetails implements Serializable { @NonNull private Map inputDetails = new HashMap<>(); + /** + * This class should be instantiated using the inputDetails received from + * {@link com.adyen.core.interfaces.PaymentRequestDetailsListener#onPaymentDetailsRequired(PaymentRequest, Collection, PaymentDetailsCallback)}. + * @param inputDetails InputDetails obtained from the SDK. + */ public PaymentDetails(Collection inputDetails) { for (InputDetail inputDetail : inputDetails) { this.inputDetails.put(inputDetail.getKey(), inputDetail); diff --git a/adyen-core/src/main/java/com/adyen/core/utils/AsyncHttpClient.java b/adyen-core/src/main/java/com/adyen/core/utils/AsyncHttpClient.java index f6f90d4378..555f4ac090 100644 --- a/adyen-core/src/main/java/com/adyen/core/utils/AsyncHttpClient.java +++ b/adyen-core/src/main/java/com/adyen/core/utils/AsyncHttpClient.java @@ -8,10 +8,13 @@ import java.util.Map; -import rx.Observable; -import rx.Subscriber; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.android.schedulers.AndroidSchedulers; /** * Utility class for handling Asynchronous HTTP requests. @@ -27,10 +30,10 @@ public static void post(@NonNull final String url, final Map hea Log.d(TAG, "POST data: " + data); final HttpClient httpClient = new HttpClient(); - Observable.create(new Observable.OnSubscribe() { + Observable.create(new ObservableOnSubscribe() { @Override - public void call(@NonNull Subscriber subscriber) { - if (subscriber.isUnsubscribed()) { + public void subscribe(@NonNull ObservableEmitter subscriber) { + if (subscriber.isDisposed()) { return; } byte[] response = null; @@ -40,16 +43,24 @@ public void call(@NonNull Subscriber subscriber) { // TODO: In general, catching a generic Exception is not a good practice. subscriber.onError(e); } - subscriber.onNext(response); - subscriber.onCompleted(); + if (response != null) { + subscriber.onNext(response); + } + subscriber.onComplete(); } }) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Subscriber() { + .subscribe(new Observer() { + @Override - public void onCompleted() { + public void onSubscribe(@io.reactivex.annotations.NonNull final Disposable d) { + // Do nothing + } + @Override + public void onComplete() { + // Do nothing } @Override diff --git a/adyen-core/src/main/java/com/adyen/core/utils/AsyncImageDownloader.java b/adyen-core/src/main/java/com/adyen/core/utils/AsyncImageDownloader.java index 35e0a8c4ff..a66ac45c3b 100644 --- a/adyen-core/src/main/java/com/adyen/core/utils/AsyncImageDownloader.java +++ b/adyen-core/src/main/java/com/adyen/core/utils/AsyncImageDownloader.java @@ -13,10 +13,13 @@ import java.io.FileNotFoundException; import java.io.InputStream; -import rx.Observable; -import rx.Subscriber; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.android.schedulers.AndroidSchedulers; /** * Utility class for downloading images and assigning them to ImageViews. @@ -43,37 +46,43 @@ public interface ImageListener { */ public static void downloadImage(final Context context, @NonNull final ImageView imageView, @NonNull final String url, @Nullable final Bitmap fallbackImage) { - Observable.create(new Observable.OnSubscribe() { + Observable.create(new ObservableOnSubscribe() { @Override - public void call(@NonNull Subscriber subscriber) { - if (subscriber.isUnsubscribed()) { + public void subscribe(@NonNull ObservableEmitter subscriber) { + if (subscriber.isDisposed()) { return; } Bitmap icon = retrieveImage(context, url, fallbackImage); subscriber.onNext(icon); - subscriber.onCompleted(); + subscriber.onComplete(); } } ).subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Subscriber() { - @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - } - - @Override - public void onNext(Bitmap response) { - imageView.setImageBitmap(response); - } - } - - ); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@io.reactivex.annotations.NonNull final Disposable d) { + // Do nothing + } + + @Override + public void onComplete() { + // Do nothing + } + + @Override + public void onError(Throwable e) { + // Do nothing + } + + @Override + public void onNext(Bitmap response) { + imageView.setImageBitmap(response); + } + } + + ); } //TODO: Following method is copy pasted from above. Reuse the code and refactor! @@ -87,27 +96,35 @@ public void onNext(Bitmap response) { */ public static void downloadImage(final Context context, @NonNull final ImageListener imageListener, @NonNull final String url, @Nullable final Bitmap fallbackImage) { - Observable.create(new Observable.OnSubscribe>() { + Observable.create(new ObservableOnSubscribe>() { @Override - public void call(@NonNull Subscriber> subscriber) { - if (subscriber.isUnsubscribed()) { + public void subscribe(@NonNull ObservableEmitter> subscriber) { + if (subscriber.isDisposed()) { return; } Bitmap icon = retrieveImage(context, url, fallbackImage); - subscriber.onNext(new Pair(icon, url)); - subscriber.onCompleted(); + subscriber.onNext(new Pair<>(icon, url)); + subscriber.onComplete(); } } ).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Subscriber>() { + .subscribe(new Observer>() { + + @Override + public void onSubscribe(@io.reactivex.annotations.NonNull final Disposable d) { + // Do nothing + } + @Override - public void onCompleted() { + public void onComplete() { + // Do nothing } @Override public void onError(Throwable e) { + // Do nothing } @Override diff --git a/adyen-ui/src/main/java/com/adyen/ui/activities/CheckoutActivity.java b/adyen-ui/src/main/java/com/adyen/ui/activities/CheckoutActivity.java index c3aebc0000..524936b12a 100644 --- a/adyen-ui/src/main/java/com/adyen/ui/activities/CheckoutActivity.java +++ b/adyen-ui/src/main/java/com/adyen/ui/activities/CheckoutActivity.java @@ -181,9 +181,6 @@ public void onPaymentMethodSelected(PaymentMethod paymentMethod) { } case CREDIT_CARD_FRAGMENT: { showActionBar(); - - final PaymentMethod paymentMethod = (PaymentMethod) intent.getSerializableExtra(PAYMENT_METHOD); - CreditCardFragment creditCardFragment = new CreditCardFragmentBuilder() .setPaymentMethod((PaymentMethod) intent.getSerializableExtra(PAYMENT_METHOD)) .setPublicKey(intent.getStringExtra(Constants.DataKeys.PUBLIC_KEY)) @@ -192,13 +189,9 @@ public void onPaymentMethodSelected(PaymentMethod paymentMethod) { .setShopperReference(intent.getStringExtra(Constants.DataKeys.SHOPPER_REFERENCE)) .setCreditCardInfoListener(new CreditCardFragment.CreditCardInfoListener() { @Override - public void onCreditCardInfoProvided(String token, boolean storeDetails) { - CreditCardPaymentDetails paymentDetails = new CreditCardPaymentDetails(paymentMethod.getInputDetails()); - paymentDetails.fillCardToken(token); - paymentDetails.fillStoreDetails(storeDetails); - + public void onCreditCardInfoProvided(CreditCardPaymentDetails creditCardPaymentDetails) { final Intent intent = new Intent(Constants.PaymentRequest.PAYMENT_DETAILS_PROVIDED_INTENT); - intent.putExtra(PAYMENT_DETAILS, paymentDetails); + intent.putExtra(PAYMENT_DETAILS, creditCardPaymentDetails); LocalBroadcastManager.getInstance(context).sendBroadcast(intent); backButtonDisabled = true; } diff --git a/adyen-ui/src/main/java/com/adyen/ui/adapters/InstallmentOptionsAdapter.java b/adyen-ui/src/main/java/com/adyen/ui/adapters/InstallmentOptionsAdapter.java new file mode 100644 index 0000000000..8b31e52ab1 --- /dev/null +++ b/adyen-ui/src/main/java/com/adyen/ui/adapters/InstallmentOptionsAdapter.java @@ -0,0 +1,72 @@ +package com.adyen.ui.adapters; + +import android.app.Activity; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import com.adyen.core.models.paymentdetails.InputDetail; + +import java.util.List; + +/** + * A custom {@link ArrayAdapter} for displaying installment options. + */ + +public class InstallmentOptionsAdapter extends ArrayAdapter { + + @NonNull + private final Activity context; + @NonNull + private final List installmentOptions; + + private static class ViewHolder { + private TextView installmentOption; + } + + public InstallmentOptionsAdapter(@NonNull Activity context, @NonNull List installmentOptions) { + super(context, android.R.layout.simple_list_item_1, installmentOptions); + this.context = context; + this.installmentOptions = installmentOptions; + } + + @Nullable + @Override + public InputDetail.Item getItem(int position) { + return installmentOptions.get(position); + } + + @Override + @NonNull + public View getView(final int position, @Nullable View view, @NonNull ViewGroup parent) { + final ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); + + viewHolder.installmentOption = (TextView) view.findViewById(android.R.id.text1); + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) view.getTag(); + } + + if (viewHolder != null) { + if (viewHolder.installmentOption != null) { + viewHolder.installmentOption.setText(installmentOptions.get(position).getName()); + } + } + return view; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return getView(position, convertView, parent); + } +} diff --git a/adyen-ui/src/main/java/com/adyen/ui/fragments/CreditCardFragment.java b/adyen-ui/src/main/java/com/adyen/ui/fragments/CreditCardFragment.java index efb9021396..3f8675ba3e 100644 --- a/adyen-ui/src/main/java/com/adyen/ui/fragments/CreditCardFragment.java +++ b/adyen-ui/src/main/java/com/adyen/ui/fragments/CreditCardFragment.java @@ -5,6 +5,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; +import android.text.TextUtils; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; @@ -13,17 +14,20 @@ import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.LinearLayout; +import android.widget.Spinner; import android.widget.TextView; import com.adyen.core.constants.Constants; import com.adyen.core.models.Amount; import com.adyen.core.models.PaymentMethod; +import com.adyen.core.models.paymentdetails.CreditCardPaymentDetails; import com.adyen.core.models.paymentdetails.InputDetail; import com.adyen.core.models.paymentdetails.PaymentDetails; import com.adyen.core.utils.AmountUtil; import com.adyen.core.utils.StringUtils; import com.adyen.ui.R; import com.adyen.ui.activities.CheckoutActivity; +import com.adyen.ui.adapters.InstallmentOptionsAdapter; import com.adyen.ui.utils.AdyenInputValidator; import com.adyen.ui.views.CVCEditText; import com.adyen.ui.views.CardHolderEditText; @@ -36,11 +40,14 @@ import org.json.JSONObject; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import adyen.com.adyencse.encrypter.ClientSideEncrypter; import adyen.com.adyencse.encrypter.exception.EncrypterException; +import static com.adyen.core.models.paymentdetails.CreditCardPaymentDetails.INSTALLMENTS; + /** * Fragment for collecting {@link PaymentDetails} for credit card payments. * Should be instantiated via {@link CreditCardFragmentBuilder}. @@ -62,6 +69,7 @@ public class CreditCardFragment extends Fragment { private CVCEditText cvcView; private CardHolderEditText cardHolderEditText; private CheckoutCheckBox saveCardCheckBox; + private Spinner installmentsSpinner; private int theme; @@ -73,10 +81,10 @@ public CreditCardFragment() { } /** - * The listener interface for receiving the (encrypted) credit card data. + * The listener interface for receiving the card payment details. */ public interface CreditCardInfoListener { - void onCreditCardInfoProvided(String token, boolean storeDetails); + void onCreditCardInfoProvided(CreditCardPaymentDetails creditCardPaymentDetails); } void setCreditCardInfoListener(@NonNull final CreditCardInfoListener creditCardInfoListener) { @@ -111,8 +119,6 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Log.d(TAG, "onCreateView()"); - final View fragmentView; final Context contextThemeWrapper = new ContextThemeWrapper(getActivity(), theme); @@ -131,6 +137,18 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, cardHolderLayout.setVisibility(View.VISIBLE); } + final Collection inputDetails = paymentMethod.getInputDetails(); + for (final InputDetail inputDetail : inputDetails) { + if (INSTALLMENTS.equals(inputDetail.getKey())) { + fragmentView.findViewById(R.id.card_installments_area).setVisibility(View.VISIBLE); + final List installmentOptions = inputDetail.getItems(); + installmentsSpinner = (Spinner) fragmentView.findViewById(R.id.installments_spinner); + final InstallmentOptionsAdapter installmentOptionsAdapter = new InstallmentOptionsAdapter(getActivity(), installmentOptions); + installmentsSpinner.setAdapter(installmentOptionsAdapter); + break; + } + } + final Button collectDataButton = (Button) fragmentView.findViewById(R.id.collectCreditCardData); final TextView checkoutTextView = (TextView) fragmentView.findViewById(R.id.amount_text_view); @@ -187,7 +205,14 @@ public void onClick(final View view) { && saveCardCheckBox.isChecked(); if (creditCardInfoListener != null) { - creditCardInfoListener.onCreditCardInfoProvided(token, storeDetails); + final CreditCardPaymentDetails creditCardPaymentDetails = new CreditCardPaymentDetails(inputDetails); + creditCardPaymentDetails.fillCardToken(token); + if (installmentsSpinner != null) { + creditCardPaymentDetails.fillNumberOfInstallments(Short.valueOf(((InputDetail.Item) installmentsSpinner + .getSelectedItem()).getId())); + } + creditCardPaymentDetails.fillStoreDetails(storeDetails); + creditCardInfoListener.onCreditCardInfoProvided(creditCardPaymentDetails); } else { Log.w(TAG, "No listener provided."); } @@ -218,6 +243,10 @@ private String getToken() { if (!inputFieldsAvailable()) { return null; } + if (TextUtils.isEmpty(publicKey)) { + Log.e(TAG, "Public key is not available; credit card payment cannot be handled."); + return ""; + } final JSONObject sensitiveData = new JSONObject(); try { @@ -238,9 +267,9 @@ private String getToken() { return encryptedData; } catch (JSONException e) { - e.printStackTrace(); + Log.e(TAG, "JSON Exception occurred while generating token.", e); } catch (EncrypterException e) { - e.printStackTrace(); + Log.e(TAG, "EncrypterException occurred while generating token.", e); } return ""; } diff --git a/adyen-ui/src/main/java/com/adyen/ui/views/AdyenIconImageView.java b/adyen-ui/src/main/java/com/adyen/ui/views/AdyenIconImageView.java index f80b11c963..6a9859fcf7 100644 --- a/adyen-ui/src/main/java/com/adyen/ui/views/AdyenIconImageView.java +++ b/adyen-ui/src/main/java/com/adyen/ui/views/AdyenIconImageView.java @@ -24,7 +24,7 @@ public AdyenIconImageView(Context context, @Nullable AttributeSet attrs, int def @Override public void setImageBitmap(Bitmap bm) { - super.setImageDrawable(IconUtil.resizeRoundCornersAndAddBorder(getContext(), bm, this.getHeight())); + super.setImageDrawable(IconUtil.resizeRoundCornersAndAddBorder(getContext(), bm, this.getMeasuredHeight())); } } diff --git a/adyen-ui/src/main/res/layout/credit_card_fragment.xml b/adyen-ui/src/main/res/layout/credit_card_fragment.xml index fe63da3ada..a9fd576bc2 100644 --- a/adyen-ui/src/main/res/layout/credit_card_fragment.xml +++ b/adyen-ui/src/main/res/layout/credit_card_fragment.xml @@ -170,6 +170,32 @@ android:focusable="true" /> + + + + + + + + + + Pay %1$s Remember card Card Details + Number of installments Payment Methods Bank account number (IBAN) NL53 ABNA 1925 1294 122 diff --git a/adyen-ui/src/main/res/values/styles.xml b/adyen-ui/src/main/res/values/styles.xml index ccf5356525..1ffd4068c0 100644 --- a/adyen-ui/src/main/res/values/styles.xml +++ b/adyen-ui/src/main/res/values/styles.xml @@ -66,6 +66,12 @@ 0.5dp + +