package com.kickstarter.libs.utils; import android.app.Activity; import android.content.Intent; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Base64; import android.view.View; import android.view.ViewGroup; 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.InstrumentInfo; import com.google.android.gms.wallet.LineItem; 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.WalletConstants; import com.google.android.gms.wallet.fragment.SupportWalletFragment; import com.google.gson.Gson; import com.kickstarter.libs.ActivityRequestCodes; import com.kickstarter.libs.Build; import com.kickstarter.libs.models.AndroidPayAuthorizedPayload; import com.kickstarter.libs.models.AndroidPayPayload; import com.kickstarter.ui.data.ActivityResult; import java.util.List; import rx.Observable; public final class AndroidPayUtils { private AndroidPayUtils() {} public static int environment(final @NonNull Build build) { return build.isInternal() ? WalletConstants.ENVIRONMENT_TEST : WalletConstants.ENVIRONMENT_PRODUCTION; } public static @NonNull MaskedWalletRequest createMaskedWalletRequest(final @NonNull AndroidPayPayload payload) { return MaskedWalletRequest.newBuilder() .setMerchantName(payload.merchantName()) .setPhoneNumberRequired(payload.phoneNumberRequired()) .setShippingAddressRequired(payload.shippingAddressRequired()) .setCurrencyCode(payload.currencyCode()) .setAllowDebitCard(payload.allowDebitCard()) .setAllowPrepaidCard(payload.allowPrepaidCard()) .setEstimatedTotalPrice(payload.estimatedTotalPrice()) .setCart( Cart.newBuilder() .setCurrencyCode(payload.cart().currencyCode()) .setTotalPrice(payload.cart().totalPrice()) .setLineItems(lineItemsFromPayload(payload)) .build() ) .setPaymentMethodTokenizationParameters( PaymentMethodTokenizationParameters.newBuilder() .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.PAYMENT_GATEWAY) .addParameter("gateway", "stripe") .addParameter("stripe:publishableKey", payload.stripePublishableKey()) .addParameter("stripe:version", payload.stripeVersion()) .build() ) .build(); } public static @NonNull FullWalletRequest createFullWalletRequest(final @NonNull String googleTransactionId, final @NonNull AndroidPayPayload payload) { return FullWalletRequest.newBuilder() .setGoogleTransactionId(googleTransactionId) .setCart( Cart.newBuilder() .setCurrencyCode(payload.cart().currencyCode()) .setTotalPrice(payload.cart().totalPrice()) .setLineItems(lineItemsFromPayload(payload)) .build() ) .build(); } /** * Returns true if the activity result contains data for a full wallet. */ public static boolean isFullWalletRequest(final @NonNull ActivityResult result) { final Intent intent = result.intent(); return intent != null && intent.getIntExtra(WalletConstants.EXTRA_ERROR_CODE, -1) == -1 && intent.hasExtra(WalletConstants.EXTRA_FULL_WALLET) && result.resultCode() == Activity.RESULT_OK && result.requestCode() == ActivityRequestCodes.CHECKOUT_ACTIVITY_WALLET_OBTAINED_FULL; } public static boolean isMaskedWalletRequest(final @NonNull ActivityResult result) { final Intent intent = result.intent(); return intent != null && intent.getIntExtra(WalletConstants.EXTRA_ERROR_CODE, -1) == -1 && result.requestCode() == ActivityRequestCodes.CHECKOUT_ACTIVITY_WALLET_REQUEST || result.requestCode() == ActivityRequestCodes.CHECKOUT_ACTIVITY_WALLET_CHANGE_REQUEST; } /** * Returns the error code contained in a wallet request. If no such error is found, `null` is returned. */ public static Integer walletRequestError(final @NonNull ActivityResult result) { final Intent intent = result.intent(); final int error = intent == null ? -1 : intent.getIntExtra(WalletConstants.EXTRA_ERROR_CODE, -1); return error == -1 ? null : error; } /** * Constructs an authorized payload that can be sent back to our server from an Android Pay full wallet. */ public static @NonNull AndroidPayAuthorizedPayload authorizedPayloadFromFullWallet(final @NonNull FullWallet fullWallet, final @NonNull Gson gson) { final InstrumentInfo instrumentInfo = fullWallet.getInstrumentInfos()[0]; return AndroidPayAuthorizedPayload .builder() .androidPayWallet( AndroidPayAuthorizedPayload.AndroidPayWallet .builder() .googleTransactionId(fullWallet.getGoogleTransactionId()) .instrumentDetails(instrumentInfo.getInstrumentDetails()) .instrumentType(instrumentInfo.getInstrumentType()) .build() ) .stripeToken(AndroidPayAuthorizedPayload.create(fullWallet.getPaymentMethodToken().getToken(), gson)) .build(); } /** * Attempts to programmatically trigger an Android Pay sheet from a wallet fragment. It does this by crawling the * subviews of the fragment and clicking them. * * NB: This is very hacky. */ public static void triggerAndroidPaySheet(final @NonNull SupportWalletFragment walletFragment) { try { final ViewGroup group = (ViewGroup) walletFragment.getView(); if (group != null) { recursiveClickFirstChildView(group); } } catch (ClassCastException | NullPointerException ignored) { } } /** * Recursive crawls the view hierarchy of `viewGroup` in order to find a clickable child and click it. */ private static boolean recursiveClickFirstChildView(final @NonNull ViewGroup viewGroup) { try { boolean continueRecursing = true; for (int idx = 0; idx < viewGroup.getChildCount() && continueRecursing; idx++) { final View child = viewGroup.getChildAt(idx); if (child.hasOnClickListeners()) { child.performClick(); return false; } else { continueRecursing = recursiveClickFirstChildView((ViewGroup) child); } } } catch (ClassCastException | NullPointerException ignored) { } return true; } private static @NonNull List<LineItem> lineItemsFromPayload(final @NonNull AndroidPayPayload payload) { return Observable.from(payload.cart().lineItems()) .map(AndroidPayUtils::lineItemFromPayloadLineItem) .toList().toBlocking().first(); } private static @NonNull LineItem lineItemFromPayloadLineItem(final @NonNull AndroidPayPayload.Cart.LineItem payloadLineItem) { return LineItem.newBuilder() .setCurrencyCode(payloadLineItem.currencyCode()) .setDescription(payloadLineItem.description()) .setQuantity(payloadLineItem.quantity()) .setTotalPrice(payloadLineItem.totalPrice()) .setUnitPrice(payloadLineItem.unitPrice()) .setRole(LineItem.Role.REGULAR) .build(); } /** * Tries to parse a payload string into a {@link AndroidPayPayload} object. * @param payloadString An (optional) string of JSON that represents the payload. * @return The parsed {@link AndroidPayPayload} object if successful, and `null` otherwise. */ public static @Nullable AndroidPayPayload payloadFromString(final @Nullable String payloadString, final @NonNull Gson gson) { if (payloadString == null) { return null; } final String json = new String(Base64.decode(payloadString, Base64.DEFAULT)); return gson.fromJson(json, AndroidPayPayload.class); } }