package com.kickstarter.ui.activities; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.text.Html; import android.util.Pair; import android.view.View; import android.webkit.WebView; import android.widget.ImageView; import android.widget.TextView; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.wallet.FullWallet; import com.google.android.gms.wallet.FullWalletRequest; import com.google.android.gms.wallet.MaskedWallet; import com.google.android.gms.wallet.MaskedWalletRequest; import com.google.android.gms.wallet.Wallet; import com.google.android.gms.wallet.WalletConstants; import com.google.android.gms.wallet.fragment.SupportWalletFragment; import com.google.android.gms.wallet.fragment.WalletFragmentInitParams; import com.google.android.gms.wallet.fragment.WalletFragmentMode; import com.google.android.gms.wallet.fragment.WalletFragmentOptions; import com.google.gson.Gson; import com.kickstarter.KSApplication; import com.kickstarter.R; import com.kickstarter.libs.ActivityRequestCodes; import com.kickstarter.libs.AndroidPayCapability; import com.kickstarter.libs.BaseActivity; import com.kickstarter.libs.Build; import com.kickstarter.libs.KSCurrency; import com.kickstarter.libs.KSString; import com.kickstarter.libs.models.AndroidPayAuthorizedPayload; import com.kickstarter.libs.models.AndroidPayPayload; import com.kickstarter.libs.qualifiers.RequiresActivityViewModel; import com.kickstarter.libs.utils.AndroidPayUtils; import com.kickstarter.libs.utils.AnimationUtils; import com.kickstarter.libs.utils.BooleanUtils; import com.kickstarter.libs.utils.ObjectUtils; import com.kickstarter.models.Project; import com.kickstarter.services.KSUri; import com.kickstarter.services.KSWebViewClient; import com.kickstarter.services.RequestHandler; import com.kickstarter.ui.IntentKey; import com.kickstarter.ui.data.LoginReason; import com.kickstarter.ui.toolbars.KSToolbar; import com.kickstarter.ui.views.ConfirmDialog; import com.kickstarter.ui.views.KSWebView; import com.kickstarter.viewmodels.CheckoutViewModel; import com.squareup.picasso.Picasso; import java.util.Arrays; import javax.inject.Inject; import butterknife.Bind; import butterknife.BindColor; import butterknife.BindString; import butterknife.ButterKnife; import butterknife.OnClick; import okhttp3.Request; import rx.android.schedulers.AndroidSchedulers; import static com.kickstarter.libs.utils.TransitionUtils.slideInFromLeft; @RequiresActivityViewModel(CheckoutViewModel.class) public final class CheckoutActivity extends BaseActivity<CheckoutViewModel> implements KSWebViewClient.Delegate { private @Nullable Project project; protected @Bind(R.id.checkout_toolbar) KSToolbar checkoutToolbar; protected @Bind(R.id.web_view) KSWebView webView; protected @Bind(R.id.checkout_loading_indicator) View loadingIndicatorView; protected @Bind(R.id.confirmation_group) View confirmationGroup; protected @Bind(R.id.pledge_disclaimer) TextView pledgeDisclaimerTextView; protected @Bind(R.id.terms_and_privacy) TextView termsAndPrivacyTextView; protected @Bind(R.id.backer_101) TextView backer101TextView; // Android pay summary bindings protected @Bind(R.id.android_pay_instrument_description) TextView androidPayInstrumentDescriptionTextView; protected @Bind(R.id.android_pay_email) TextView androidPayEmailTextView; // Project context view bindings protected @Bind(R.id.project_context_image_view) ImageView contextPhotoImageView; protected @Bind(R.id.project_context_creator_name) TextView creatorNameTextView; protected @Bind(R.id.project_context_project_name) TextView projectNameTextView; protected @BindString(R.string.profile_settings_about_terms) String termsOfUseString; protected @BindString(R.string.profile_settings_about_privacy) String privacyPolicyString; protected @BindString(R.string.project_checkout_android_pay_pledge_disclaimer) String pledgeDisclaimerString; protected @BindString(R.string.project_checkout_android_pay_terms_and_privacy) String termsAndPrivacyString; protected @BindString(R.string.project_checkout_android_pay_backer_101) String backer101String; protected @BindString(R.string.project_creator_by_creator) String projectCreatorByCreatorString; protected @BindString(R.string.project_checkout_android_pay_error_title) String androidPayErrorTitleString; protected @BindString(R.string.project_checkout_android_pay_error_message) String androidPayErrorMessageString; protected @BindColor(R.color.white) int whiteColor; protected @Inject KSCurrency ksCurrency; protected @Inject KSString ksString; protected @Inject Gson gson; protected @Inject AndroidPayCapability androidPayCapability; protected @Inject Build build; private @Nullable SupportWalletFragment walletFragment; private @Nullable SupportWalletFragment confirmationWalletFragment; private @Nullable GoogleApiClient googleApiClient; private boolean isInAndroidPayFlow; @Override protected void onCreate(final @Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.checkout_layout); ButterKnife.bind(this); ((KSApplication) getApplication()).component().inject(this); webView.client().setDelegate(this); webView.client().registerRequestHandlers(Arrays.asList( new RequestHandler(KSUri::isCheckoutThanksUri, this::handleCheckoutThanksUriRequest), new RequestHandler(KSUri::isSignupUri, this::handleSignupUriRequest) )); viewModel.outputs.project() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(project -> this.project = project); viewModel.outputs.title() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(checkoutToolbar::setTitle); viewModel.outputs.isAndroidPayAvailable() .filter(BooleanUtils::isTrue) .take(1) .subscribe(__ -> setGoogleApiClient()); viewModel.outputs.isAndroidPayAvailable() .filter(BooleanUtils::isTrue) .take(1) .subscribe(__ -> this.prepareWalletFragment()); viewModel.outputs.showAndroidPaySheet() .filter(ObjectUtils::isNotNull) .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::showAndroidPaySheet); viewModel.outputs.displayAndroidPayConfirmation() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::showOrHideAndroidPayConfirmation); viewModel.outputs.updateAndroidPayConfirmation() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(wp -> updateAndroidPayConfirmation(wp.first, wp.second)); viewModel.outputs.popActivityOffStack() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(__ -> this.popActivity()); viewModel.outputs.attemptAndroidPayConfirmation() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(wp -> this.attemptAndroidPayConfirmation(wp.first, wp.second)); viewModel.outputs.completeAndroidPay() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::androidPayComplete); viewModel.errors.androidPayError() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::showAndroidPayError); } @Override protected void onResume() { super.onResume(); viewModel.outputs.url() .take(1) .filter(__ -> !isInAndroidPayFlow) .subscribe(webView::loadUrl); } @Override public void back() { isInAndroidPayFlow = false; viewModel.inputs.backButtonClicked(); } private void popActivity() { super.back(); } /** * This method is called from {@link com.kickstarter.services.KSWebViewClient} when an Android Pay * payload has been obtained from the webview. */ public void takeAndroidPayPayloadString(final @Nullable String payloadString) { viewModel.inputs.takePayloadString(payloadString); } private boolean handleCheckoutThanksUriRequest(final @NonNull Request request, final @NonNull WebView webView) { final Intent intent = new Intent(this, ThanksActivity.class) .putExtra(IntentKey.PROJECT, project); startActivityWithTransition(intent, R.anim.slide_in_right, R.anim.fade_out_slide_out_left); return true; } private boolean handleSignupUriRequest(final @NonNull Request request, final @NonNull WebView webView) { final Intent intent = new Intent(this, LoginToutActivity.class) .putExtra(IntentKey.LOGIN_REASON, LoginReason.BACK_PROJECT); startActivityForResult(intent, ActivityRequestCodes.LOGIN_FLOW); return true; } /** * Call when the android pay sheet should be shown. */ private void showAndroidPaySheet(final @NonNull AndroidPayPayload payload) { if (walletFragment == null) { return; } isInAndroidPayFlow = true; final MaskedWalletRequest request = AndroidPayUtils.createMaskedWalletRequest(payload); walletFragment.initialize( WalletFragmentInitParams.newBuilder() .setMaskedWalletRequest(request) .setMaskedWalletRequestCode(ActivityRequestCodes.CHECKOUT_ACTIVITY_WALLET_REQUEST) .build() ); AndroidPayUtils.triggerAndroidPaySheet(walletFragment); } /** * Creates and injects a wallet fragment into the activity. */ private void prepareWalletFragment() { final WalletFragmentOptions walletFragmentOptions = WalletFragmentOptions.newBuilder() .setEnvironment(AndroidPayUtils.environment(build)) .setTheme(WalletConstants.THEME_LIGHT) .setMode(WalletFragmentMode.BUY_BUTTON) .build(); walletFragment = SupportWalletFragment.newInstance(walletFragmentOptions); getSupportFragmentManager().beginTransaction() .replace(R.id.masked_wallet_fragment, walletFragment) .commit(); } private void setGoogleApiClient() { googleApiClient = new GoogleApiClient.Builder(this) .enableAutoManage(this, null) .addApi(Wallet.API, new Wallet.WalletOptions.Builder() .setEnvironment(AndroidPayUtils.environment(build)) .setTheme(WalletConstants.THEME_LIGHT) .build()) .build(); } private void showOrHideAndroidPayConfirmation(final boolean visible) { if (visible) { webView.setVisibility(View.GONE); confirmationGroup.setVisibility(View.VISIBLE); } else { webView.setVisibility(View.VISIBLE); confirmationGroup.setVisibility(View.GONE); } } /** * Call when a masked wallet has been obtained and the content in the android pay confirmation should be rendered. */ private void updateAndroidPayConfirmation(final @NonNull MaskedWallet maskedWallet, final @NonNull AndroidPayPayload payload) { Picasso.with(this).load(project.photo().full()).into(contextPhotoImageView); projectNameTextView.setText(project.name()); creatorNameTextView.setText(ksString.format( projectCreatorByCreatorString, "creator_name", project.creator().name() )); termsAndPrivacyTextView.setText(Html.fromHtml(termsAndPrivacyString)); backer101TextView.setText(Html.fromHtml(backer101String)); if (maskedWallet != null) { androidPayEmailTextView.setText(maskedWallet.getEmail()); final String[] paymentDescriptions = maskedWallet.getPaymentDescriptions(); if (paymentDescriptions.length > 0) { androidPayInstrumentDescriptionTextView.setText(paymentDescriptions[0]); } } pledgeDisclaimerTextView.setText(Html.fromHtml( ksString.format(pledgeDisclaimerString, "charge_amount", ksCurrency.format( Float.valueOf(payload.cart().totalPrice()), project ) ) )); confirmationWalletFragment = SupportWalletFragment.newInstance( WalletFragmentOptions.newBuilder() .setEnvironment(AndroidPayUtils.environment(build)) .setTheme(WalletConstants.THEME_LIGHT) .setMode(WalletFragmentMode.SELECTION_DETAILS) .build() ); confirmationWalletFragment.initialize( WalletFragmentInitParams.newBuilder() .setMaskedWallet(maskedWallet) .setMaskedWalletRequestCode(ActivityRequestCodes.CHECKOUT_ACTIVITY_WALLET_CHANGE_REQUEST) .build() ); getSupportFragmentManager().beginTransaction() .replace(R.id.confirmation_masked_wallet_fragment, confirmationWalletFragment) .commit(); } private void attemptAndroidPayConfirmation(final @NonNull MaskedWallet maskedWallet, final @NonNull AndroidPayPayload payload) { final FullWalletRequest fullWalletRequest = AndroidPayUtils.createFullWalletRequest( maskedWallet.getGoogleTransactionId(), payload ); Wallet.Payments.loadFullWallet(googleApiClient, fullWalletRequest, ActivityRequestCodes.CHECKOUT_ACTIVITY_WALLET_OBTAINED_FULL); } @SuppressLint("NewApi") private void androidPayComplete(final @NonNull FullWallet fullWallet) { final AndroidPayAuthorizedPayload authorizedPayload = AndroidPayUtils.authorizedPayloadFromFullWallet(fullWallet, gson); final String json = gson.toJson(authorizedPayload, AndroidPayAuthorizedPayload.class); // TODO: is this an injection problem? final String javascript = String.format("checkout_android_pay_next(%s);", json); webView.evaluateJavascript(javascript, null); } private void showAndroidPayError(final @NonNull Integer error) { final ConfirmDialog dialog = new ConfirmDialog(this, androidPayErrorTitleString + " (" + error.toString() + ")", androidPayErrorMessageString); dialog.setOnDismissListener(d -> this.back()); dialog.show(); } @OnClick(R.id.back_button) protected void toolbarBackButtonClicked() { viewModel.inputs.backButtonClicked(); } @OnClick(R.id.android_pay_confirmation_button) protected void androidPayConfirmationClicked() { viewModel.inputs.confirmAndroidPayClicked(); } @OnClick(R.id.android_pay_change) protected void androidPayChangeClicked() { if (confirmationWalletFragment != null) { AndroidPayUtils.triggerAndroidPaySheet(confirmationWalletFragment); } } @OnClick(R.id.terms_and_privacy) protected void termsAndPrivacyClicked() { final CharSequence[] items = new CharSequence[] { termsOfUseString, privacyPolicyString }; new AlertDialog.Builder(this) .setItems(items, (__, which) -> { final Intent intent; if (which == 0) { intent = new Intent(this, HelpActivity.Terms.class); } else { intent = new Intent(this, HelpActivity.Privacy.class); } startActivityWithTransition(intent, R.anim.slide_in_right, R.anim.fade_out_slide_out_left); }) .show(); } @OnClick(R.id.backer_101) protected void backer101Clicked() { final Intent intent = new Intent(this, WebViewActivity.class); intent.putExtra(IntentKey.URL, Uri.parse(project.webProjectUrl()) .buildUpon() .appendEncodedPath("pledge/big_print") .build() .toString() ); startActivityWithTransition(intent, R.anim.slide_in_right, R.anim.fade_out_slide_out_left); } @Override public void webViewExternalLinkActivated(final @NonNull KSWebViewClient webViewClient, final @NonNull String url) {} @Override public void webViewOnPageStarted(final @NonNull KSWebViewClient webViewClient, final @Nullable String url) { loadingIndicatorView.startAnimation(AnimationUtils.INSTANCE.appearAnimation()); } @Override public void webViewOnPageFinished(final @NonNull KSWebViewClient webViewClient, final @Nullable String url) { loadingIndicatorView.startAnimation(AnimationUtils.INSTANCE.disappearAnimation()); } @Override public void webViewPageIntercepted(final @NonNull KSWebViewClient webViewClient, final @NonNull String url) { viewModel.inputs.pageIntercepted(url); } @Override protected @Nullable Pair<Integer, Integer> exitTransition() { return slideInFromLeft(); } }