From 09714fb2c96abbc91a427c62945db17c2cc0d32f Mon Sep 17 00:00:00 2001 From: Evan Liu <454746257@qq.com> Date: Wed, 25 May 2022 06:37:03 +0800 Subject: [PATCH] Add support for Fragments (#117) --- README.md | 79 ++++ .../main/java/com/affirm/android/Affirm.java | 341 ++++++++++++++++-- .../com/affirm/android/AffirmActivity.java | 73 ++-- .../java/com/affirm/android/AffirmClient.java | 6 +- .../com/affirm/android/AffirmConstants.java | 1 + .../com/affirm/android/AffirmFragment.java | 82 +++++ .../com/affirm/android/AffirmTrackView.java | 6 +- .../java/com/affirm/android/AffirmUtils.java | 10 + .../com/affirm/android/CheckoutActivity.java | 57 +-- .../affirm/android/CheckoutBaseActivity.java | 42 +-- .../affirm/android/CheckoutBaseFragment.java | 54 +++ .../com/affirm/android/CheckoutFragment.java | 149 ++++++++ .../com/affirm/android/ModalActivity.java | 150 ++------ .../com/affirm/android/ModalFragment.java | 184 ++++++++++ .../com/affirm/android/PrequalActivity.java | 79 +--- .../com/affirm/android/PrequalFragment.java | 152 ++++++++ .../com/affirm/android/PromotionWebView.java | 5 +- .../affirm/android/VcnCheckoutActivity.java | 120 +----- .../affirm/android/VcnCheckoutFragment.java | 224 ++++++++++++ ...ebview.xml => affirm_fragment_webview.xml} | 9 +- .../samples/MainActivityEspressoTest.java | 208 +++++++++++ samples-java/src/main/AndroidManifest.xml | 8 + .../main/java/com/affirm/samples/Config.java | 8 + .../samples/FragmentUsagesActivity.java | 14 + .../samples/FragmentUsagesFragment.java | 169 +++++++++ .../java/com/affirm/samples/MainFragment.java | 4 +- .../com/affirm/samples/SampleApplication.java | 2 +- .../res/layout/activity_fragment_usages.xml | 4 + .../res/layout/fragment_fragment_usages.xml | 66 ++++ .../src/main/res/layout/fragment_main.xml | 9 + samples-java/src/main/res/values/strings.xml | 1 + 31 files changed, 1879 insertions(+), 437 deletions(-) create mode 100644 affirm/src/main/java/com/affirm/android/AffirmFragment.java create mode 100644 affirm/src/main/java/com/affirm/android/CheckoutBaseFragment.java create mode 100644 affirm/src/main/java/com/affirm/android/CheckoutFragment.java create mode 100644 affirm/src/main/java/com/affirm/android/ModalFragment.java create mode 100644 affirm/src/main/java/com/affirm/android/PrequalFragment.java create mode 100644 affirm/src/main/java/com/affirm/android/VcnCheckoutFragment.java rename affirm/src/main/res/layout/{affirm_activity_webview.xml => affirm_fragment_webview.xml} (80%) create mode 100644 samples-java/src/main/java/com/affirm/samples/Config.java create mode 100644 samples-java/src/main/java/com/affirm/samples/FragmentUsagesActivity.java create mode 100644 samples-java/src/main/java/com/affirm/samples/FragmentUsagesFragment.java create mode 100755 samples-java/src/main/res/layout/activity_fragment_usages.xml create mode 100755 samples-java/src/main/res/layout/fragment_fragment_usages.xml diff --git a/README.md b/README.md index 1c45b761..5d04ef6f 100755 --- a/README.md +++ b/README.md @@ -268,6 +268,85 @@ final AffirmTrack affirmTrack = AffirmTrack.builder() Affirm.trackOrderConfirmed(MainActivity.this, trackModel()); ``` +## Fragment supports +We also support using fragment directly, only need to pass a ViewGroup id, we will put the `AffirmFragment` in this specified view. + +- Checkout +```java + // In your activity/fragment, you need to implement Affirm.CheckoutCallbacks + Affirm.startCheckout(this, R.id.container, checkoutModel(), null, 10, false); + + // - Affirm.CheckoutCallbacks + @Override + public void onAffirmCheckoutSuccess(@NonNull String token) { + Toast.makeText(this, "Checkout token: " + token, Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmCheckoutCancelled() { + Toast.makeText(this, "Checkout Cancelled", Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmCheckoutError(@Nullable String message) { + Toast.makeText(this, "Checkout Error: " + message, Toast.LENGTH_LONG).show(); + } +``` + +- VCN checkout +```java + // In your activity/fragment, you need to implement Affirm.VcnCheckoutCallbacks + Affirm.startCheckout(this, R.id.container, checkoutModel(), null, 10, true); + + // - Affirm.VcnCheckoutCallbacks + @Override + public void onAffirmVcnCheckoutCancelled() { + Toast.makeText(this, "Vcn Checkout Cancelled", Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmVcnCheckoutCancelledReason(@NonNull VcnReason vcnReason) { + Toast.makeText(this, "Vcn Checkout Cancelled: " + vcnReason.toString(), Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmVcnCheckoutError(@Nullable String message) { + Toast.makeText(this, "Vcn Checkout Error: " + message, Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmVcnCheckoutSuccess(@NonNull CardDetails cardDetails) { + Toast.makeText(this, "Vcn Checkout Card: " + cardDetails.toString(), Toast.LENGTH_LONG).show(); + } +``` + +- Promotion +```java + AffirmPromotionButton affirmPromotionButton = findViewById(R.id.promo); + Affirm.configureWithAmount(this, R.id.container, affirmPromotionButton, null, PromoPageType.PRODUCT, PRICE, true, null); +``` + +- Site modal +```java + // In your activity/fragment, you need to implement Affirm.PrequalCallbacks + Affirm.showSiteModal(this, R.id.container, null, "5LNMQ33SEUYHLNUC"); + + @Override + public void onAffirmPrequalError(@Nullable String message) { + Toast.makeText(this, "Prequal Error: " + message, Toast.LENGTH_LONG).show(); + } +``` +- Product modal +```java + // In your activity/fragment, you need to implement Affirm.PrequalCallbacks + Affirm.showProductModal(this, R.id.container, PRICE, null, PromoPageType.PRODUCT, null) + + @Override + public void onAffirmPrequalError(@Nullable String message) { + Toast.makeText(this, "Prequal Error: " + message, Toast.LENGTH_LONG).show(); + } +``` + - Since there is no callback, it will return success after 10 seconds timeout - We will replace using the HTTP API after the API is done diff --git a/affirm/src/main/java/com/affirm/android/Affirm.java b/affirm/src/main/java/com/affirm/android/Affirm.java index 40942daf..da3346a2 100755 --- a/affirm/src/main/java/com/affirm/android/Affirm.java +++ b/affirm/src/main/java/com/affirm/android/Affirm.java @@ -8,8 +8,10 @@ import android.view.View; import android.view.ViewGroup; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import com.affirm.android.exception.AffirmException; @@ -39,8 +41,8 @@ import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_CREATION_CLICK; import static com.affirm.android.AffirmTracker.TrackingLevel.INFO; import static com.affirm.android.AffirmTracker.createTrackingCheckout; -import static com.affirm.android.ModalActivity.ModalType.PRODUCT; -import static com.affirm.android.ModalActivity.ModalType.SITE; +import static com.affirm.android.ModalFragment.ModalType.PRODUCT; +import static com.affirm.android.ModalFragment.ModalType.SITE; public final class Affirm { @@ -732,7 +734,7 @@ public static void startCheckout(@NonNull Fragment fragment, @NonNull Checkout c if (useVCN) { AffirmTracker.track(VCN_CHECKOUT_CREATION_CLICK, INFO, trackInfo); VcnCheckoutActivity.startActivity(fragment, vcnCheckoutRequest, checkout, caas, - null, cardAuthWindow, receiveReasonCodes, false); + null, cardAuthWindow, false); } else { AffirmTracker.track(CHECKOUT_WEBVIEW_CLICK, INFO, trackInfo); CheckoutActivity.startActivity(fragment, checkoutRequest, checkout, caas, @@ -894,9 +896,130 @@ protected static void startVcnCheckout(@NonNull Activity activity, @NonNull Chec @Nullable String caas, @Nullable Money money, boolean newFlow, int cardAuthWindow) { VcnCheckoutActivity.startActivity(activity, vcnCheckoutRequest, checkout, caas, money, - cardAuthWindow, receiveReasonCodes, newFlow); + cardAuthWindow, newFlow); } + /** + * Use a `AffirmFragment` to start checkout flow/ vcn checkout flow. And in you host activity, + * you need implements CheckoutCallbacks / VcnCheckoutCallbacks + * + * @param activity activity {@link AppCompatActivity} + * @param containerViewId The specified view will contain the fragment + * @param checkout checkout object that contains address & shipping info & others... + * @param caas caas merchant-level attribute + * @param cardAuthWindow the value is a positive integer, 0 being a valid value + * @param useVCN Start VCN checkout or not + * @return a `AffirmFragment` + */ + public static AffirmFragment startCheckout(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @NonNull Checkout checkout, + @Nullable String caas, + int cardAuthWindow, + boolean useVCN) { + if (useVCN) { + return VcnCheckoutFragment.newInstance(activity, + containerViewId, checkout, receiveReasonCodes, caas, null, + cardAuthWindow, false); + } else { + return CheckoutFragment.newInstance(activity, + containerViewId, checkout, caas, cardAuthWindow); + } + } + + /** + * Use a `AffirmFragment` to start checkout flow/ vcn checkout flow. And in you host activity, + * you need implements CheckoutCallbacks / VcnCheckoutCallbacks + * + * @param fragment fragment {@link Fragment} + * @param containerViewId The specified view will contain the fragment + * @param checkout checkout object that contains address & shipping info & others... + * @param caas caas merchant-level attribute + * @param cardAuthWindow the value is a positive integer, 0 being a valid value + * @param useVCN Start VCN checkout or not + * @return a `AffirmFragment` + */ + public static AffirmFragment startCheckout(@NonNull Fragment fragment, + @IdRes int containerViewId, + @NonNull Checkout checkout, + @Nullable String caas, + int cardAuthWindow, + boolean useVCN) { + if (useVCN) { + return VcnCheckoutFragment.newInstance(fragment, + containerViewId, checkout, receiveReasonCodes, caas, null, + cardAuthWindow, false); + } else { + return CheckoutFragment.newInstance(fragment, + containerViewId, checkout, caas, cardAuthWindow); + } + } + + /** + * Use a `AffirmFragment` to start checkout flow/ vcn checkout flow. And in you host activity, + * you need implements CheckoutCallbacks / VcnCheckoutCallbacks + * + * @param activity activity {@link AppCompatActivity} + * @param containerViewId The specified view will contain the fragment + * @param checkout checkout object that contains address & shipping info & others... + * @param caas caas merchant-level attribute + * @param money momoney that user inputney + * @param cardAuthWindow the value is a positive integer, 0 being a valid value + * @param newFlow new flow + * @param useVCN Start VCN checkout or not + * @return a `AffirmFragment` + */ + protected static AffirmFragment startCheckout(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @NonNull Checkout checkout, + @Nullable String caas, + @Nullable Money money, + int cardAuthWindow, + boolean newFlow, + boolean useVCN) { + if (useVCN) { + return VcnCheckoutFragment.newInstance(activity, + containerViewId, checkout, receiveReasonCodes, caas, money, cardAuthWindow, + newFlow); + } else { + return CheckoutFragment.newInstance(activity, + containerViewId, checkout, caas, cardAuthWindow); + } + } + + /** + * Use a `AffirmFragment` to start checkout flow/ vcn checkout flow. And in you host activity, + * you need implements CheckoutCallbacks / VcnCheckoutCallbacks + * + * @param fragment fragment {@link Fragment} + * @param containerViewId The specified view will contain the fragment + * @param checkout checkout object that contains address & shipping info & others... + * @param caas caas merchant-level attribute + * @param money money that user input + * @param cardAuthWindow the value is a positive integer, 0 being a valid value + * @param newFlow new flow + * @param useVCN Start VCN checkout or not + * @return a `AffirmFragment` + */ + protected static AffirmFragment startCheckout(@NonNull Fragment fragment, + @IdRes int containerViewId, + @NonNull Checkout checkout, + @Nullable String caas, + @Nullable Money money, + int cardAuthWindow, + boolean newFlow, + boolean useVCN) { + if (useVCN) { + return VcnCheckoutFragment.newInstance(fragment, + containerViewId, checkout, receiveReasonCodes, caas, money, cardAuthWindow, + newFlow); + } else { + return CheckoutFragment.newInstance(fragment, + containerViewId, checkout, caas, cardAuthWindow); + } + } + + /** * Start site modal * @@ -947,6 +1070,43 @@ public static void showSiteModal(@NonNull Fragment fragment, @Nullable String mo pageType != null ? pageType.getType() : null, promoId); } + /** + * Use a `AffirmFragment` to start site modal. + * + * @param activity activity {@link AppCompatActivity} + * @param containerViewId The specified view will contain the fragment + * @param pageType need to use one of "banner, cart, category, homepage, landing, + * payment, product, search" + * @param modalId the client's modal id + * @return a `ModalFragment` + */ + public static AffirmFragment showSiteModal(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @Nullable PromoPageType pageType, + @Nullable String modalId) { + return ModalFragment.newInstance(activity, containerViewId, BigDecimal.valueOf(0.0), SITE, + modalId, pageType != null ? pageType.getType() : null, null); + } + + /** + * Use a `AffirmFragment` to start site modal. + * + * @param fragment activity {@link Fragment} + * @param containerViewId The specified view will contain the fragment + * @param pageType need to use one of "banner, cart, category, homepage, landing, + * payment, product, search" + * @param modalId the client's modal id + * @return a `ModalFragment` + */ + public static AffirmFragment showSiteModal(@NonNull Fragment fragment, + @IdRes int containerViewId, + @Nullable PromoPageType pageType, + @Nullable String modalId) { + return ModalFragment.newInstance(fragment, containerViewId, BigDecimal.valueOf(0.0), SITE, + modalId, pageType != null ? pageType.getType() : null, null); + } + + /** * Start product modal * @@ -1009,6 +1169,50 @@ public static void showProductModal(@NonNull Fragment fragment, pageType != null ? pageType.getType() : null, promoId); } + /** + * Use a `AffirmFragment` to start product modal. + * + * @param activity activity {@link AppCompatActivity} + * @param containerViewId The specified view will contain the fragment + * @param amount (Float) eg 112.02 as $112 and ¢2 + * @param modalId the client's modal id + * @param pageType need to use one of "banner, cart, category, homepage, landing, + * payment, product, search" + * @param promoId the client's promo id + * @return a `ModalFragment` + */ + public static AffirmFragment showProductModal(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + BigDecimal amount, + @Nullable String modalId, + @Nullable PromoPageType pageType, + @Nullable String promoId) { + return ModalFragment.newInstance(activity, containerViewId, amount, PRODUCT, modalId, + pageType != null ? pageType.getType() : null, promoId); + } + + /** + * Use a `AffirmFragment` to start product modal. + * + * @param fragment activity {@link Fragment} + * @param containerViewId The specified view will contain the fragment + * @param amount (Float) eg 112.02 as $112 and ¢2 + * @param modalId the client's modal id + * @param pageType need to use one of "banner, cart, category, homepage, landing, + * payment, product, search" + * @param promoId the client's promo id + * @return a `ModalFragment` + */ + public static AffirmFragment showProductModal(@NonNull Fragment fragment, + @IdRes int containerViewId, + BigDecimal amount, + @Nullable String modalId, + @Nullable PromoPageType pageType, + @Nullable String promoId) { + return ModalFragment.newInstance(fragment, containerViewId, amount, PRODUCT, modalId, + pageType != null ? pageType.getType() : null, promoId); + } + /** * Write the as low as span (text and logo) on a AffirmPromoLabel * @@ -1139,6 +1343,118 @@ public static void configureWithAmount(@NonNull final AffirmPromotionButton prom final boolean showCta, @Nullable final List items) { AffirmUtils.requireNonNull(promotionButton, "AffirmPromotionButton cannot be null"); + final View.OnClickListener onClickListener = promotionView -> { + Activity activity = AffirmUtils.getActivityFromView(promotionView); + if (activity == null || promotionButton.isEmpty()) { + return; + } + boolean showPrequal = (boolean) promotionView.getTag(); + String type = pageType != null ? pageType.getType() : null; + if (showPrequal) { + PrequalActivity.startActivity(activity, + prequalRequest, amount, promoId, type); + } else { + ModalActivity.startActivity(activity, + prequalRequest, amount, PRODUCT, null, + type, promoId); + } + }; + configureWithAmount(promotionButton, promoId, pageType, amount, showCta, items, + onClickListener); + } + + /** + * Write the as low as span (text and logo) on a AffirmPromoLabel + * + * @param activity activity {@link AppCompatActivity} + * @param containerViewId The specified view will contain the fragment + * @param promotionButton AffirmPromotionButton to show the promo message + * @param promoId the client's promo id + * @param pageType need to use one of "banner, cart, category, homepage, landing, + * payment, product, search" + * @param amount (Float) eg 112.02 as $112 and ¢2 + * @param showCta whether need to show cta + * @param items A list of item objects. + */ + public static void configureWithAmount( + @NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @NonNull final AffirmPromotionButton promotionButton, + @Nullable final String promoId, + @Nullable final PromoPageType pageType, + final BigDecimal amount, + final boolean showCta, + @Nullable final List items) { + + final View.OnClickListener onClickListener = promotionView -> { + if (promotionButton.isEmpty()) { + return; + } + boolean showPrequal = (boolean) promotionView.getTag(); + String type = pageType != null ? pageType.getType() : null; + if (showPrequal) { + PrequalFragment.newInstance(activity, containerViewId, amount, + promoId, type); + } else { + ModalFragment.newInstance(activity, containerViewId, amount, + PRODUCT, null, type, promoId); + } + }; + configureWithAmount(promotionButton, promoId, pageType, amount, showCta, items, + onClickListener); + } + + /** + * Write the as low as span (text and logo) on a AffirmPromoLabel + * + * @param fragment activity {@link Fragment} + * @param containerViewId The specified view will contain the fragment + * @param promotionButton AffirmPromotionButton to show the promo message + * @param promoId the client's promo id + * @param pageType need to use one of "banner, cart, category, homepage, landing, + * payment, product, search" + * @param amount (Float) eg 112.02 as $112 and ¢2 + * @param showCta whether need to show cta + * @param items A list of item objects. + */ + public static void configureWithAmount( + @NonNull Fragment fragment, + @IdRes int containerViewId, + @NonNull final AffirmPromotionButton promotionButton, + @Nullable final String promoId, + @Nullable final PromoPageType pageType, + final BigDecimal amount, + final boolean showCta, + @Nullable final List items) { + + final View.OnClickListener onClickListener = promotionView -> { + if (promotionButton.isEmpty()) { + return; + } + boolean showPrequal = (boolean) promotionView.getTag(); + String type = pageType != null ? pageType.getType() : null; + if (showPrequal) { + PrequalFragment.newInstance(fragment, containerViewId, amount, + promoId, type); + } else { + ModalFragment.newInstance(fragment, containerViewId, amount, + PRODUCT, null, type, promoId); + } + }; + configureWithAmount(promotionButton, promoId, pageType, amount, showCta, items, + onClickListener); + } + + private static void configureWithAmount( + @NonNull final AffirmPromotionButton promotionButton, + @Nullable final String promoId, + @Nullable final PromoPageType pageType, + final BigDecimal amount, + final boolean showCta, + @Nullable final List items, + View.OnClickListener onClickListener) { + AffirmUtils.requireNonNull(promotionButton, + "AffirmPromotionButton cannot be null"); final SpannablePromoCallback callback = new SpannablePromoCallback() { @Override public void onPromoWritten(@NonNull final String promoMessage, @@ -1176,7 +1492,6 @@ public void onStop() { @Override public void onDestroy() { affirmPromoRequest.cancel(); - promotionButton.destroy(); } }; @@ -1208,22 +1523,6 @@ public void onViewDetachedFromWindow(View v) { }); affirmPromoRequest.create(); - - final View.OnClickListener onClickListener = v -> { - Activity activity = AffirmUtils.getActivityFromView(v); - if (activity == null || promotionButton.isEmpty()) { - return; - } - boolean showPrequal = (boolean) v.getTag(); - String type = pageType != null ? pageType.getType() : null; - if (showPrequal) { - PrequalActivity.startActivity(activity, prequalRequest, amount, - promoId, type); - } else { - ModalActivity.startActivity(activity, prequalRequest, amount, - PRODUCT, null, type, promoId); - } - }; promotionButton.setOnClickListener(onClickListener); } diff --git a/affirm/src/main/java/com/affirm/android/AffirmActivity.java b/affirm/src/main/java/com/affirm/android/AffirmActivity.java index 0802384d..281fc3a1 100755 --- a/affirm/src/main/java/com/affirm/android/AffirmActivity.java +++ b/affirm/src/main/java/com/affirm/android/AffirmActivity.java @@ -1,19 +1,24 @@ package com.affirm.android; +import static com.affirm.android.Affirm.RESULT_ERROR; +import static com.affirm.android.AffirmConstants.CHECKOUT_ERROR; +import static com.affirm.android.AffirmConstants.CHECKOUT_TOKEN; +import static com.affirm.android.AffirmConstants.CREDIT_DETAILS; +import static com.affirm.android.AffirmConstants.PREQUAL_ERROR; +import static com.affirm.android.AffirmConstants.VCN_REASON; + import android.app.Activity; import android.content.Intent; -import android.os.Bundle; -import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; -abstract class AffirmActivity extends AppCompatActivity implements AffirmWebChromeClient.Callbacks { +import com.affirm.android.model.CardDetails; +import com.affirm.android.model.VcnReason; - AffirmWebView webView; - View progressIndicator; +abstract class AffirmActivity extends AppCompatActivity { static void startForResult(@NonNull Activity originalActivity, @NonNull Intent intent, @@ -28,39 +33,43 @@ static void startForResult(@NonNull Fragment originalFragment, originalFragment.startActivityForResult(intent, requestCode); } - abstract void initViews(); - - abstract void beforeOnCreate(); - - abstract void initData(@Nullable Bundle savedInstanceState); - - abstract void onAttached(); - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - beforeOnCreate(); - super.onCreate(savedInstanceState); - - setContentView(R.layout.affirm_activity_webview); - webView = findViewById(R.id.webview); - progressIndicator = findViewById(R.id.progressIndicator); + protected void finishWithError(@Nullable String error) { + final Intent intent = new Intent(); + intent.putExtra(CHECKOUT_ERROR, error); + setResult(RESULT_ERROR, intent); + finish(); + } - initViews(); + protected void finishWithToken(String token) { + final Intent intent = new Intent(); + intent.putExtra(CHECKOUT_TOKEN, token); + setResult(Activity.RESULT_OK, intent); + finish(); + } - initData(savedInstanceState); + protected void finishWithCancellation() { + setResult(Activity.RESULT_CANCELED); + finish(); + } - onAttached(); + protected void finishWithCardDetail(@NonNull CardDetails cardDetails) { + final Intent intent = new Intent(); + intent.putExtra(CREDIT_DETAILS, cardDetails); + setResult(Activity.RESULT_OK, intent); + finish(); } - @Override - protected void onDestroy() { - webView.destroyWebView(); - webView = null; - super.onDestroy(); + protected void finishWithVcnReason(@NonNull VcnReason vcnReason) { + final Intent intent = new Intent(); + intent.putExtra(VCN_REASON, vcnReason); + setResult(Activity.RESULT_CANCELED, intent); + finish(); } - @Override - public void chromeLoadCompleted() { - progressIndicator.setVisibility(View.GONE); + protected void finishWithPrequalError(@Nullable String error) { + final Intent intent = new Intent(); + intent.putExtra(PREQUAL_ERROR, error); + setResult(RESULT_ERROR, intent); + finish(); } } diff --git a/affirm/src/main/java/com/affirm/android/AffirmClient.java b/affirm/src/main/java/com/affirm/android/AffirmClient.java index 51d2601e..cb4921f9 100644 --- a/affirm/src/main/java/com/affirm/android/AffirmClient.java +++ b/affirm/src/main/java/com/affirm/android/AffirmClient.java @@ -91,8 +91,10 @@ public void onResponse(@NotNull Call call, @NotNull Response response) { ); } catch (JsonSyntaxException | IOException e) { handleErrorResponse( - new APIException("Some error occurred while parsing the " - + "promo response", e), listener + new APIException("Some error occurred while parsing response", + e), + listener + ); } } else { diff --git a/affirm/src/main/java/com/affirm/android/AffirmConstants.java b/affirm/src/main/java/com/affirm/android/AffirmConstants.java index 9ecbd10c..29a810a0 100755 --- a/affirm/src/main/java/com/affirm/android/AffirmConstants.java +++ b/affirm/src/main/java/com/affirm/android/AffirmConstants.java @@ -169,6 +169,7 @@ static String getProductionInvalidCheckoutRedirectUrl() { static final String CHECKOUT_TOKEN = "checkout_token"; static final String CHECKOUT_ERROR = "checkout_error"; static final String CHECKOUT_EXTRA = "checkout_extra"; + static final String CHECKOUT_RECEIVE_REASON_CODES = "checkout_receive_reason_codes"; static final String CHECKOUT_CAAS_EXTRA = "checkout_caas_extra"; static final String CHECKOUT_MONEY = "checkout_money"; static final String CHECKOUT_CARD_AUTH_WINDOW = "checkout_card_auth_window"; diff --git a/affirm/src/main/java/com/affirm/android/AffirmFragment.java b/affirm/src/main/java/com/affirm/android/AffirmFragment.java new file mode 100644 index 00000000..3abd2751 --- /dev/null +++ b/affirm/src/main/java/com/affirm/android/AffirmFragment.java @@ -0,0 +1,82 @@ +package com.affirm.android; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +abstract class AffirmFragment extends Fragment implements AffirmWebChromeClient.Callbacks { + + protected static final String TAG_PREFIX = "AffirmFragment"; + + AffirmWebView webView; + private View progressIndicator; + + abstract void initViews(); + + abstract void onAttached(); + + protected static void addFragment(FragmentManager fragmentManager, @IdRes int containerViewId, + @NonNull Fragment fragment, @NonNull String tag) { + fragmentManager + .beginTransaction() + .add(containerViewId, fragment, tag) + .commitAllowingStateLoss(); + fragmentManager.executePendingTransactions(); + } + + protected void removeFragment(@NonNull String tag) { + FragmentManager fragmentManager = getFragmentManager(); + if (fragmentManager == null) { + AffirmLog.d("The fragment is getting detached from the Activity"); + return; + } + Fragment fragment = fragmentManager.findFragmentByTag(tag); + if (fragment != null) { + fragmentManager.beginTransaction().remove(fragment).commit(); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.affirm_fragment_webview, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + webView = view.findViewById(R.id.webview); + progressIndicator = view.findViewById(R.id.progressIndicator); + + AffirmUtils.debuggableWebView(getContext()); + initViews(); + onAttached(); + } + + @Override + public void onDestroy() { + webView.destroyWebView(); + webView = null; + super.onDestroy(); + } + + @Override + public void chromeLoadCompleted() { + progressIndicator.setVisibility(View.GONE); + } +} \ No newline at end of file diff --git a/affirm/src/main/java/com/affirm/android/AffirmTrackView.java b/affirm/src/main/java/com/affirm/android/AffirmTrackView.java index 1e469c4d..33e9bc44 100755 --- a/affirm/src/main/java/com/affirm/android/AffirmTrackView.java +++ b/affirm/src/main/java/com/affirm/android/AffirmTrackView.java @@ -92,12 +92,14 @@ public void run() { private String initialHtml() { String html; + InputStream ins = null; try { - final InputStream ins = - getResources().openRawResource(R.raw.affirm_track_order_confirmed); + ins = getResources().openRawResource(R.raw.affirm_track_order_confirmed); html = AffirmUtils.readInputStream(ins); } catch (IOException e) { throw new RuntimeException(e); + } finally { + AffirmUtils.closeInputStream(ins); } final String fullPath = HTTPS_PROTOCOL + AffirmPlugins.get().baseJsUrl() + JS_PATH; diff --git a/affirm/src/main/java/com/affirm/android/AffirmUtils.java b/affirm/src/main/java/com/affirm/android/AffirmUtils.java index 9596e482..3fbfa529 100755 --- a/affirm/src/main/java/com/affirm/android/AffirmUtils.java +++ b/affirm/src/main/java/com/affirm/android/AffirmUtils.java @@ -55,6 +55,16 @@ static String readInputStream(@NonNull InputStream inputStream) throws IOExcepti return total.toString(); } + static void closeInputStream(@Nullable InputStream inputStream) { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + AffirmLog.e("Failed to close InputStream!", e); + } + } + } + static String replacePlaceholders(@NonNull String text, @NonNull Map map) { for (Object o : map.entrySet()) { Map.Entry pair = (Map.Entry) o; diff --git a/affirm/src/main/java/com/affirm/android/CheckoutActivity.java b/affirm/src/main/java/com/affirm/android/CheckoutActivity.java index a2dcd490..605149a6 100755 --- a/affirm/src/main/java/com/affirm/android/CheckoutActivity.java +++ b/affirm/src/main/java/com/affirm/android/CheckoutActivity.java @@ -3,10 +3,7 @@ import android.app.Activity; import android.content.Intent; -import com.affirm.android.exception.AffirmException; -import com.affirm.android.exception.ConnectionException; import com.affirm.android.model.Checkout; -import com.affirm.android.model.CheckoutResponse; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -15,17 +12,8 @@ import static com.affirm.android.AffirmConstants.CHECKOUT_CAAS_EXTRA; import static com.affirm.android.AffirmConstants.CHECKOUT_CARD_AUTH_WINDOW; import static com.affirm.android.AffirmConstants.CHECKOUT_EXTRA; -import static com.affirm.android.AffirmConstants.CHECKOUT_TOKEN; -import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_CREATION_FAIL; -import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_CREATION_SUCCESS; -import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_WEBVIEW_FAIL; -import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_WEBVIEW_SUCCESS; -import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; -import static com.affirm.android.AffirmTracker.TrackingLevel.INFO; -import static com.affirm.android.AffirmTracker.createTrackingException; -public class CheckoutActivity extends CheckoutBaseActivity - implements CheckoutWebViewClient.Callbacks { +public class CheckoutActivity extends CheckoutBaseActivity implements Affirm.CheckoutCallbacks { static void startActivity(@NonNull Activity activity, int requestCode, @NonNull Checkout checkout, @Nullable String caas, @@ -53,54 +41,23 @@ private static Intent buildIntent( return intent; } - @Override - void initViews() { - AffirmUtils.debuggableWebView(this); - webView.setWebViewClient(new CheckoutWebViewClient(this)); - webView.setWebChromeClient(new AffirmWebChromeClient(this)); - } - @Override boolean useVCN() { return false; } @Override - InnerCheckoutCallback getInnerCheckoutCallback() { - return new InnerCheckoutCallback() { - @Override - public void onError(@NonNull AffirmException exception) { - AffirmTracker.track(CHECKOUT_CREATION_FAIL, ERROR, - createTrackingException(exception)); - finishWithError(exception); - } - - @Override - public void onSuccess(@NonNull CheckoutResponse response) { - AffirmTracker.track(CHECKOUT_CREATION_SUCCESS, INFO, null); - webView.loadUrl(response.redirectUrl()); - } - }; - } - - @Override - public void onWebViewError(@NonNull ConnectionException error) { - AffirmTracker.track(CHECKOUT_WEBVIEW_FAIL, ERROR, createTrackingException(error)); - finishWithError(error); + public void onAffirmCheckoutError(@Nullable String message) { + finishWithError(message); } @Override - public void onWebViewConfirmation(@NonNull String token) { - AffirmTracker.track(CHECKOUT_WEBVIEW_SUCCESS, INFO, null); - - final Intent intent = new Intent(); - intent.putExtra(CHECKOUT_TOKEN, token); - setResult(RESULT_OK, intent); - finish(); + public void onAffirmCheckoutCancelled() { + finishWithCancellation(); } @Override - public void onWebViewCancellation() { - webViewCancellation(); + public void onAffirmCheckoutSuccess(@NonNull String token) { + finishWithToken(token); } } diff --git a/affirm/src/main/java/com/affirm/android/CheckoutBaseActivity.java b/affirm/src/main/java/com/affirm/android/CheckoutBaseActivity.java index d4d8e787..05044eca 100755 --- a/affirm/src/main/java/com/affirm/android/CheckoutBaseActivity.java +++ b/affirm/src/main/java/com/affirm/android/CheckoutBaseActivity.java @@ -1,6 +1,5 @@ package com.affirm.android; -import android.content.Intent; import android.os.Bundle; import androidx.annotation.NonNull; @@ -10,18 +9,14 @@ import org.joda.money.Money; -import static com.affirm.android.Affirm.RESULT_ERROR; import static com.affirm.android.AffirmConstants.CHECKOUT_CAAS_EXTRA; import static com.affirm.android.AffirmConstants.CHECKOUT_CARD_AUTH_WINDOW; -import static com.affirm.android.AffirmConstants.CHECKOUT_ERROR; import static com.affirm.android.AffirmConstants.CHECKOUT_EXTRA; import static com.affirm.android.AffirmConstants.CHECKOUT_MONEY; import static com.affirm.android.AffirmConstants.NEW_FLOW; abstract class CheckoutBaseActivity extends AffirmActivity { - private CheckoutRequest checkoutRequest; - protected Checkout checkout; private Money money; @@ -34,14 +29,10 @@ abstract class CheckoutBaseActivity extends AffirmActivity { abstract boolean useVCN(); - abstract InnerCheckoutCallback getInnerCheckoutCallback(); - @Override - void beforeOnCreate() { - } + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - @Override - void initData(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { checkout = savedInstanceState.getParcelable(CHECKOUT_EXTRA); caas = savedInstanceState.getString(CHECKOUT_CAAS_EXTRA); @@ -55,6 +46,9 @@ void initData(@Nullable Bundle savedInstanceState) { newFlow = getIntent().getBooleanExtra(NEW_FLOW, false); cardAuthWindow = getIntent().getIntExtra(CHECKOUT_CARD_AUTH_WINDOW, -1); } + + Affirm.startCheckout(this, android.R.id.content, checkout, caas, money, cardAuthWindow, + newFlow, useVCN()); } @Override @@ -67,30 +61,4 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(NEW_FLOW, newFlow); outState.putInt(CHECKOUT_CARD_AUTH_WINDOW, cardAuthWindow); } - - @Override - void onAttached() { - checkoutRequest = new CheckoutRequest(checkout, getInnerCheckoutCallback(), caas, money, - useVCN(), cardAuthWindow); - checkoutRequest.create(); - } - - @Override - protected void onDestroy() { - checkoutRequest.cancel(); - super.onDestroy(); - } - - protected void finishWithError(@NonNull Throwable error) { - final Intent intent = new Intent(); - intent.putExtra(CHECKOUT_ERROR, error.toString()); - setResult(RESULT_ERROR, intent); - finish(); - } - - protected void webViewCancellation() { - setResult(RESULT_CANCELED); - finish(); - } - } diff --git a/affirm/src/main/java/com/affirm/android/CheckoutBaseFragment.java b/affirm/src/main/java/com/affirm/android/CheckoutBaseFragment.java new file mode 100644 index 00000000..c7fb7887 --- /dev/null +++ b/affirm/src/main/java/com/affirm/android/CheckoutBaseFragment.java @@ -0,0 +1,54 @@ +package com.affirm.android; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.affirm.android.model.Checkout; + +import static com.affirm.android.AffirmConstants.CHECKOUT_CAAS_EXTRA; +import static com.affirm.android.AffirmConstants.CHECKOUT_CARD_AUTH_WINDOW; +import static com.affirm.android.AffirmConstants.CHECKOUT_EXTRA; +import static com.affirm.android.AffirmConstants.CHECKOUT_MONEY; + +import org.joda.money.Money; + +abstract class CheckoutBaseFragment extends AffirmFragment { + + private CheckoutRequest checkoutRequest; + + private Checkout checkout; + + private Money money; + + private String caas; + + private int cardAuthWindow; + + abstract InnerCheckoutCallback innerCheckoutCallback(); + + abstract boolean useVCN(); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + checkout = getArguments().getParcelable(CHECKOUT_EXTRA); + caas = getArguments().getString(CHECKOUT_CAAS_EXTRA); + money = (Money) getArguments().getSerializable(CHECKOUT_MONEY); + cardAuthWindow = getArguments().getInt(CHECKOUT_CARD_AUTH_WINDOW, -1); + } + + @Override + void onAttached() { + checkoutRequest = new CheckoutRequest(checkout, innerCheckoutCallback(), caas, money, + useVCN(), cardAuthWindow); + checkoutRequest.create(); + } + + @Override + public void onDestroy() { + checkoutRequest.cancel(); + super.onDestroy(); + } +} \ No newline at end of file diff --git a/affirm/src/main/java/com/affirm/android/CheckoutFragment.java b/affirm/src/main/java/com/affirm/android/CheckoutFragment.java new file mode 100644 index 00000000..f9002ba9 --- /dev/null +++ b/affirm/src/main/java/com/affirm/android/CheckoutFragment.java @@ -0,0 +1,149 @@ +package com.affirm.android; + +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.affirm.android.exception.AffirmException; +import com.affirm.android.exception.ConnectionException; +import com.affirm.android.model.Checkout; +import com.affirm.android.model.CheckoutResponse; + +import static com.affirm.android.AffirmConstants.CHECKOUT_CAAS_EXTRA; +import static com.affirm.android.AffirmConstants.CHECKOUT_CARD_AUTH_WINDOW; +import static com.affirm.android.AffirmConstants.CHECKOUT_EXTRA; +import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_CREATION_FAIL; +import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_CREATION_SUCCESS; +import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_WEBVIEW_FAIL; +import static com.affirm.android.AffirmTracker.TrackingEvent.CHECKOUT_WEBVIEW_SUCCESS; +import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; +import static com.affirm.android.AffirmTracker.TrackingLevel.INFO; +import static com.affirm.android.AffirmTracker.createTrackingException; + +public final class CheckoutFragment extends CheckoutBaseFragment + implements CheckoutWebViewClient.Callbacks { + + private static final String CHECKOUT = "Checkout"; + private static final String TAG = TAG_PREFIX + "." + CHECKOUT; + + private Affirm.CheckoutCallbacks listener; + + static CheckoutFragment newInstance(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @NonNull Checkout checkout, + @Nullable String caas, + int cardAuthWindow) { + return newInstance(activity.getSupportFragmentManager(), containerViewId, checkout, + caas, cardAuthWindow); + } + + static CheckoutFragment newInstance(@NonNull Fragment fragment, + @IdRes int containerViewId, + @NonNull Checkout checkout, + @Nullable String caas, + int cardAuthWindow) { + return newInstance(fragment.getChildFragmentManager(), containerViewId, checkout, + caas, cardAuthWindow); + } + + private static CheckoutFragment newInstance(@NonNull FragmentManager fragmentManager, + @IdRes int containerViewId, + @NonNull Checkout checkout, + @Nullable String caas, + int cardAuthWindow) { + if (fragmentManager.findFragmentByTag(TAG) != null) { + return (CheckoutFragment) fragmentManager.findFragmentByTag(TAG); + } + + CheckoutFragment fragment = new CheckoutFragment(); + Bundle bundle = new Bundle(); + bundle.putParcelable(CHECKOUT_EXTRA, checkout); + bundle.putString(CHECKOUT_CAAS_EXTRA, caas); + bundle.putInt(CHECKOUT_CARD_AUTH_WINDOW, cardAuthWindow); + + fragment.setArguments(bundle); + + addFragment(fragmentManager, containerViewId, fragment, TAG); + return fragment; + } + + @Override + void initViews() { + webView.setWebViewClient(new CheckoutWebViewClient(this)); + webView.setWebChromeClient(new AffirmWebChromeClient(this)); + } + + @Override + boolean useVCN() { + return false; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof Affirm.CheckoutCallbacks) { + listener = (Affirm.CheckoutCallbacks) context; + } else if (getParentFragment() instanceof Affirm.CheckoutCallbacks) { + listener = (Affirm.CheckoutCallbacks) getParentFragment(); + } + } + + @Override + public void onDetach() { + listener = null; + super.onDetach(); + } + + @Override + InnerCheckoutCallback innerCheckoutCallback() { + return new InnerCheckoutCallback() { + @Override + public void onError(@NonNull AffirmException exception) { + AffirmTracker.track(CHECKOUT_CREATION_FAIL, ERROR, + createTrackingException(exception)); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmCheckoutError(exception.toString()); + } + } + + @Override + public void onSuccess(@NonNull CheckoutResponse response) { + AffirmTracker.track(CHECKOUT_CREATION_SUCCESS, INFO, null); + webView.loadUrl(response.redirectUrl()); + } + }; + } + + @Override + public void onWebViewError(@NonNull ConnectionException error) { + AffirmTracker.track(CHECKOUT_WEBVIEW_FAIL, ERROR, createTrackingException(error)); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmCheckoutError(error.toString()); + } + } + + @Override + public void onWebViewConfirmation(@NonNull String token) { + AffirmTracker.track(CHECKOUT_WEBVIEW_SUCCESS, INFO, null); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmCheckoutSuccess(token); + } + } + + @Override + public void onWebViewCancellation() { + removeFragment(TAG); + if (listener != null) { + listener.onAffirmCheckoutCancelled(); + } + } +} \ No newline at end of file diff --git a/affirm/src/main/java/com/affirm/android/ModalActivity.java b/affirm/src/main/java/com/affirm/android/ModalActivity.java index e53fbcaf..faaa015d 100755 --- a/affirm/src/main/java/com/affirm/android/ModalActivity.java +++ b/affirm/src/main/java/com/affirm/android/ModalActivity.java @@ -3,70 +3,35 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; - -import com.affirm.android.exception.ConnectionException; - -import java.io.IOException; -import java.io.InputStream; import java.math.BigDecimal; -import java.util.HashMap; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RawRes; import androidx.fragment.app.Fragment; - -import static com.affirm.android.Affirm.RESULT_ERROR; -import static com.affirm.android.AffirmConstants.AFFIRM_CHECKOUT_CANCELLATION_URL; import static com.affirm.android.AffirmConstants.AMOUNT; -import static com.affirm.android.AffirmConstants.API_KEY; -import static com.affirm.android.AffirmConstants.CANCEL_URL; -import static com.affirm.android.AffirmConstants.HTTPS_PROTOCOL; -import static com.affirm.android.AffirmConstants.JAVASCRIPT; -import static com.affirm.android.AffirmConstants.JS_PATH; -import static com.affirm.android.AffirmConstants.MAP_EXTRA; import static com.affirm.android.AffirmConstants.MODAL_ID; import static com.affirm.android.AffirmConstants.PAGE_TYPE; -import static com.affirm.android.AffirmConstants.PREQUAL_ERROR; import static com.affirm.android.AffirmConstants.PROMO_ID; -import static com.affirm.android.AffirmConstants.TEXT_HTML; import static com.affirm.android.AffirmConstants.TYPE_EXTRA; -import static com.affirm.android.AffirmConstants.UTF_8; -import static com.affirm.android.AffirmTracker.TrackingEvent.PRODUCT_WEBVIEW_FAIL; -import static com.affirm.android.AffirmTracker.TrackingEvent.SITE_WEBVIEW_FAIL; -import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; -import static com.affirm.android.AffirmTracker.createTrackingException; -public class ModalActivity extends AffirmActivity implements ModalWebViewClient.Callbacks { +public class ModalActivity extends AffirmActivity implements Affirm.PrequalCallbacks { - private ModalType type; - private HashMap map; - - enum ModalType { - PRODUCT(R.raw.affirm_modal_template, PRODUCT_WEBVIEW_FAIL), - SITE(R.raw.affirm_modal_template, SITE_WEBVIEW_FAIL); - - @RawRes - final int templateRes; - final AffirmTracker.TrackingEvent failureEvent; - - ModalType(int templateRes, AffirmTracker.TrackingEvent failureEvent) { - this.templateRes = templateRes; - this.failureEvent = failureEvent; - } - } + private ModalFragment.ModalType type; + private BigDecimal amount; + private String modalId; + private String pageType; + private String promoId; static void startActivity(@NonNull Activity activity, int requestCode, BigDecimal amount, - ModalType type, @Nullable String modalId, @Nullable String pageType, - @Nullable String promoId) { + ModalFragment.ModalType type, @Nullable String modalId, + @Nullable String pageType, @Nullable String promoId) { Intent intent = buildIntent(activity, amount, type, modalId, pageType, promoId); startForResult(activity, intent, requestCode); } static void startActivity(@NonNull Fragment fragment, int requestCode, BigDecimal amount, - ModalType type, @Nullable String modalId, @Nullable String pageType, - @Nullable String promoId) { + ModalFragment.ModalType type, @Nullable String modalId, + @Nullable String pageType, @Nullable String promoId) { Intent intent = buildIntent(fragment.requireActivity(), amount, type, modalId, pageType, promoId); startForResult(fragment, intent, requestCode); @@ -75,95 +40,52 @@ static void startActivity(@NonNull Fragment fragment, int requestCode, BigDecima private static Intent buildIntent( @NonNull Activity originalActivity, BigDecimal amount, - ModalType type, + ModalFragment.ModalType type, @Nullable String modalId, @Nullable String pageType, @Nullable String promoId) { final Intent intent = new Intent(originalActivity, ModalActivity.class); - final String stringAmount = - String.valueOf(AffirmUtils.decimalDollarsToIntegerCents(amount)); - final String fullPath = HTTPS_PROTOCOL + AffirmPlugins.get().baseJsUrl() + JS_PATH; - - final HashMap map = new HashMap<>(); - map.put(AMOUNT, stringAmount); - map.put(API_KEY, AffirmPlugins.get().publicKey()); - map.put(JAVASCRIPT, fullPath); - map.put(CANCEL_URL, AFFIRM_CHECKOUT_CANCELLATION_URL); - map.put(MODAL_ID, modalId == null ? "" : modalId); - map.put(PAGE_TYPE, pageType == null ? "" : pageType); - map.put(PROMO_ID, promoId == null ? "" : promoId); - + intent.putExtra(AMOUNT, amount); intent.putExtra(TYPE_EXTRA, type); - intent.putExtra(MAP_EXTRA, map); + intent.putExtra(MODAL_ID, modalId == null ? "" : modalId); + intent.putExtra(PAGE_TYPE, pageType == null ? "" : pageType); + intent.putExtra(PROMO_ID, promoId == null ? "" : promoId); return intent; } @Override - void beforeOnCreate() { - } - - @Override - void initViews() { - AffirmUtils.debuggableWebView(this); - webView.setWebViewClient(new ModalWebViewClient(this)); - webView.setWebChromeClient(new AffirmWebChromeClient(this)); - } + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - @Override - void initData(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { - map = (HashMap) savedInstanceState.getSerializable(MAP_EXTRA); - type = (ModalType) savedInstanceState.getSerializable(TYPE_EXTRA); + amount = (BigDecimal) savedInstanceState.getSerializable(AMOUNT); + type = (ModalFragment.ModalType) savedInstanceState.getSerializable(TYPE_EXTRA); + modalId = savedInstanceState.getString(MODAL_ID); + pageType = savedInstanceState.getString(PAGE_TYPE); + promoId = savedInstanceState.getString(PROMO_ID); } else { - map = (HashMap) getIntent().getSerializableExtra(MAP_EXTRA); - type = (ModalType) getIntent().getSerializableExtra(TYPE_EXTRA); + amount = (BigDecimal) getIntent().getSerializableExtra(AMOUNT); + type = (ModalFragment.ModalType) getIntent().getSerializableExtra(TYPE_EXTRA); + modalId = getIntent().getStringExtra(MODAL_ID); + pageType = getIntent().getStringExtra(PAGE_TYPE); + promoId = getIntent().getStringExtra(PROMO_ID); } - } - - @Override - void onAttached() { - final String html = initialHtml(); - webView.loadDataWithBaseURL( - HTTPS_PROTOCOL + AffirmPlugins.get().basePromoUrl(), - html, TEXT_HTML, UTF_8, null); + ModalFragment.newInstance(this, android.R.id.content, amount, type, modalId, + pageType, promoId); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - - outState.putInt(TYPE_EXTRA, type.templateRes); - outState.putSerializable(MAP_EXTRA, map); - } - - private String initialHtml() { - String html; - try { - final InputStream ins = getResources().openRawResource(type.templateRes); - html = AffirmUtils.readInputStream(ins); - } catch (IOException e) { - throw new RuntimeException(e); - } - - return AffirmUtils.replacePlaceholders(html, map); + outState.putSerializable(AMOUNT, amount); + outState.putSerializable(TYPE_EXTRA, type); + outState.putString(MODAL_ID, modalId == null ? "" : modalId); + outState.putString(PAGE_TYPE, pageType == null ? "" : pageType); + outState.putString(PROMO_ID, promoId == null ? "" : promoId); } @Override - public void onWebViewCancellation() { - setResult(RESULT_CANCELED); - finish(); - } - - @Override - public void onWebViewError(@NonNull ConnectionException error) { - AffirmTracker.track(type.failureEvent, ERROR, createTrackingException(error)); - finish(); - - final Intent intent = new Intent(); - intent.putExtra(PREQUAL_ERROR, error.toString()); - setResult(RESULT_ERROR, intent); - finish(); + public void onAffirmPrequalError(@Nullable String message) { + finishWithPrequalError(message); } } - - diff --git a/affirm/src/main/java/com/affirm/android/ModalFragment.java b/affirm/src/main/java/com/affirm/android/ModalFragment.java new file mode 100644 index 00000000..3c4ba1f5 --- /dev/null +++ b/affirm/src/main/java/com/affirm/android/ModalFragment.java @@ -0,0 +1,184 @@ +package com.affirm.android; + +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RawRes; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.affirm.android.exception.ConnectionException; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.HashMap; + +import static com.affirm.android.AffirmConstants.AFFIRM_CHECKOUT_CANCELLATION_URL; +import static com.affirm.android.AffirmConstants.AMOUNT; +import static com.affirm.android.AffirmConstants.API_KEY; +import static com.affirm.android.AffirmConstants.CANCEL_URL; +import static com.affirm.android.AffirmConstants.HTTPS_PROTOCOL; +import static com.affirm.android.AffirmConstants.JAVASCRIPT; +import static com.affirm.android.AffirmConstants.JS_PATH; +import static com.affirm.android.AffirmConstants.MAP_EXTRA; +import static com.affirm.android.AffirmConstants.MODAL_ID; +import static com.affirm.android.AffirmConstants.PAGE_TYPE; +import static com.affirm.android.AffirmConstants.PROMO_ID; +import static com.affirm.android.AffirmConstants.TEXT_HTML; +import static com.affirm.android.AffirmConstants.TYPE_EXTRA; +import static com.affirm.android.AffirmConstants.UTF_8; +import static com.affirm.android.AffirmTracker.TrackingEvent.PRODUCT_WEBVIEW_FAIL; +import static com.affirm.android.AffirmTracker.TrackingEvent.SITE_WEBVIEW_FAIL; +import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; +import static com.affirm.android.AffirmTracker.createTrackingException; + +public final class ModalFragment extends AffirmFragment implements ModalWebViewClient.Callbacks { + + public enum ModalType { + PRODUCT(R.raw.affirm_modal_template, PRODUCT_WEBVIEW_FAIL), + SITE(R.raw.affirm_modal_template, SITE_WEBVIEW_FAIL); + + @RawRes + final int templateRes; + final AffirmTracker.TrackingEvent failureEvent; + + ModalType(int templateRes, AffirmTracker.TrackingEvent failureEvent) { + this.templateRes = templateRes; + this.failureEvent = failureEvent; + } + } + + private static final String MODAL = "Modal"; + private static final String TAG = TAG_PREFIX + "." + MODAL; + + private ModalType type; + + private HashMap map; + + private Affirm.PrequalCallbacks listener; + + static AffirmFragment newInstance(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @NonNull BigDecimal amount, @NonNull ModalType type, + @Nullable String modalId, @Nullable String pageType, + @Nullable String promoId) { + return newInstance(activity.getSupportFragmentManager(), containerViewId, amount, type, + modalId, pageType, promoId); + } + + static ModalFragment newInstance(@NonNull Fragment fragment, + @IdRes int containerViewId, + @NonNull BigDecimal amount, @NonNull ModalType type, + @Nullable String modalId, @Nullable String pageType, + @Nullable String promoId) { + return newInstance(fragment.getChildFragmentManager(), containerViewId, amount, + type, modalId, pageType, promoId); + } + + private static ModalFragment newInstance(@NonNull FragmentManager fragmentManager, + @IdRes int containerViewId, + @NonNull BigDecimal amount, @NonNull ModalType type, + @Nullable String modalId, @Nullable String pageType, + @Nullable String promoId) { + if (fragmentManager.findFragmentByTag(TAG) != null) { + return (ModalFragment) fragmentManager.findFragmentByTag(TAG); + } + + final String stringAmount = + String.valueOf(AffirmUtils.decimalDollarsToIntegerCents(amount)); + final String fullPath = HTTPS_PROTOCOL + AffirmPlugins.get().baseJsUrl() + JS_PATH; + + final HashMap map = new HashMap<>(); + map.put(AMOUNT, stringAmount); + map.put(API_KEY, AffirmPlugins.get().publicKey()); + map.put(JAVASCRIPT, fullPath); + map.put(CANCEL_URL, AFFIRM_CHECKOUT_CANCELLATION_URL); + map.put(MODAL_ID, modalId == null ? "" : modalId); + map.put(PAGE_TYPE, pageType == null ? "" : pageType); + map.put(PROMO_ID, promoId == null ? "" : promoId); + + ModalFragment fragment = new ModalFragment(); + Bundle bundle = new Bundle(); + bundle.putSerializable(TYPE_EXTRA, type); + bundle.putSerializable(MAP_EXTRA, map); + fragment.setArguments(bundle); + + addFragment(fragmentManager, containerViewId, fragment, TAG); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + map = (HashMap) getArguments().getSerializable(MAP_EXTRA); + type = (ModalType) getArguments().getSerializable(TYPE_EXTRA); + } + + @Override + void initViews() { + webView.setWebViewClient(new ModalWebViewClient(this)); + webView.setWebChromeClient(new AffirmWebChromeClient(this)); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof Affirm.PrequalCallbacks) { + listener = (Affirm.PrequalCallbacks) context; + } else if (getParentFragment() instanceof Affirm.PrequalCallbacks) { + listener = (Affirm.PrequalCallbacks) getParentFragment(); + } + } + + @Override + public void onDetach() { + listener = null; + super.onDetach(); + } + + @Override + void onAttached() { + final String html = initialHtml(); + webView.loadDataWithBaseURL( + HTTPS_PROTOCOL + AffirmPlugins.get().basePromoUrl(), + html, TEXT_HTML, UTF_8, null); + } + + private String initialHtml() { + String html; + InputStream ins = null; + try { + ins = getResources().openRawResource(type.templateRes); + html = AffirmUtils.readInputStream(ins); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + AffirmUtils.closeInputStream(ins); + } + + return AffirmUtils.replacePlaceholders(html, map); + } + + @Override + public void onWebViewCancellation() { + removeFragment(TAG); + if (getActivity() != null && getActivity() instanceof ModalActivity) { + getActivity().finish(); + } + } + + @Override + public void onWebViewError(@NonNull ConnectionException error) { + AffirmTracker.track(type.failureEvent, ERROR, createTrackingException(error)); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmPrequalError(error.toString()); + } + } +} \ No newline at end of file diff --git a/affirm/src/main/java/com/affirm/android/PrequalActivity.java b/affirm/src/main/java/com/affirm/android/PrequalActivity.java index 3327f4cc..443ff70d 100755 --- a/affirm/src/main/java/com/affirm/android/PrequalActivity.java +++ b/affirm/src/main/java/com/affirm/android/PrequalActivity.java @@ -2,7 +2,6 @@ import android.app.Activity; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; @@ -10,32 +9,15 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import com.affirm.android.exception.ConnectionException; - import java.math.BigDecimal; -import static com.affirm.android.Affirm.RESULT_ERROR; import static com.affirm.android.AffirmConstants.AMOUNT; -import static com.affirm.android.AffirmConstants.HTTPS_PROTOCOL; import static com.affirm.android.AffirmConstants.PAGE_TYPE; -import static com.affirm.android.AffirmConstants.PREQUAL_ERROR; -import static com.affirm.android.AffirmConstants.PREQUAL_IS_SDK; -import static com.affirm.android.AffirmConstants.PREQUAL_PAGE_TYPE; -import static com.affirm.android.AffirmConstants.PREQUAL_PATH; -import static com.affirm.android.AffirmConstants.PREQUAL_PROMO_EXTERNAL_ID; -import static com.affirm.android.AffirmConstants.PREQUAL_PUBLIC_API_KEY; -import static com.affirm.android.AffirmConstants.PREQUAL_REFERRING_URL; -import static com.affirm.android.AffirmConstants.PREQUAL_UNIT_PRICE; -import static com.affirm.android.AffirmConstants.PREQUAL_USE_PROMO; import static com.affirm.android.AffirmConstants.PROMO_ID; -import static com.affirm.android.AffirmConstants.REFERRING_URL; -import static com.affirm.android.AffirmTracker.TrackingEvent.PREQUAL_WEBVIEW_FAIL; -import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; -import static com.affirm.android.AffirmTracker.createTrackingException; -public class PrequalActivity extends AffirmActivity implements PrequalWebViewClient.Callbacks { +public class PrequalActivity extends AffirmActivity implements Affirm.PrequalCallbacks { - private String amount; + private BigDecimal amount; private String promoId; private String pageType; @@ -59,75 +41,36 @@ private static Intent buildIntent( @Nullable String promoId, @Nullable String pageType) { final Intent intent = new Intent(originalActivity, PrequalActivity.class); - final String stringAmount = - String.valueOf(AffirmUtils.decimalDollarsToIntegerCents(amount)); - intent.putExtra(AMOUNT, stringAmount); + intent.putExtra(AMOUNT, amount); intent.putExtra(PROMO_ID, promoId); intent.putExtra(PAGE_TYPE, pageType); return intent; } @Override - void beforeOnCreate() { - } - - @Override - void initViews() { - AffirmUtils.debuggableWebView(this); - webView.setWebViewClient(new PrequalWebViewClient(this)); - webView.setWebChromeClient(new AffirmWebChromeClient(this)); - } - - @Override - void initData(@Nullable Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); if (savedInstanceState != null) { - amount = savedInstanceState.getString(AMOUNT); + amount = (BigDecimal) savedInstanceState.getSerializable(AMOUNT); promoId = savedInstanceState.getString(PROMO_ID); pageType = savedInstanceState.getString(PAGE_TYPE); } else { - amount = getIntent().getStringExtra(AMOUNT); + amount = (BigDecimal) getIntent().getSerializableExtra(AMOUNT); promoId = getIntent().getStringExtra(PROMO_ID); pageType = getIntent().getStringExtra(PAGE_TYPE); } - } - - @Override - void onAttached() { - String publicKey = AffirmPlugins.get().publicKey(); - String prequalUri = HTTPS_PROTOCOL + AffirmPlugins.get().basePromoUrl() + PREQUAL_PATH; - Uri.Builder builder = Uri.parse(prequalUri).buildUpon(); - builder.appendQueryParameter(PREQUAL_PUBLIC_API_KEY, publicKey); - builder.appendQueryParameter(PREQUAL_UNIT_PRICE, amount); - builder.appendQueryParameter(PREQUAL_USE_PROMO, "true"); - builder.appendQueryParameter(PREQUAL_IS_SDK, "true"); - builder.appendQueryParameter(PREQUAL_REFERRING_URL, REFERRING_URL); - if (promoId != null) { - builder.appendQueryParameter(PREQUAL_PROMO_EXTERNAL_ID, promoId); - } - if (pageType != null) { - builder.appendQueryParameter(PREQUAL_PAGE_TYPE, pageType); - } - webView.loadUrl(builder.build().toString()); + PrequalFragment.newInstance(this, android.R.id.content, amount, promoId, pageType); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putString(AMOUNT, amount); + outState.putSerializable(AMOUNT, amount); outState.putString(PROMO_ID, promoId); outState.putString(PAGE_TYPE, pageType); } - @Override - public void onWebViewError(@NonNull ConnectionException error) { - AffirmTracker.track(PREQUAL_WEBVIEW_FAIL, ERROR, createTrackingException(error)); - final Intent intent = new Intent(); - intent.putExtra(PREQUAL_ERROR, error.toString()); - setResult(RESULT_ERROR, intent); - finish(); - } - @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { @@ -138,7 +81,7 @@ public boolean onOptionsItemSelected(MenuItem item) { } @Override - public void onWebViewConfirmation() { - finish(); + public void onAffirmPrequalError(@Nullable String message) { + finishWithPrequalError(message); } } diff --git a/affirm/src/main/java/com/affirm/android/PrequalFragment.java b/affirm/src/main/java/com/affirm/android/PrequalFragment.java new file mode 100644 index 00000000..545cf95e --- /dev/null +++ b/affirm/src/main/java/com/affirm/android/PrequalFragment.java @@ -0,0 +1,152 @@ +package com.affirm.android; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.affirm.android.exception.ConnectionException; + +import java.math.BigDecimal; + +import static com.affirm.android.AffirmConstants.AMOUNT; +import static com.affirm.android.AffirmConstants.HTTPS_PROTOCOL; +import static com.affirm.android.AffirmConstants.PAGE_TYPE; +import static com.affirm.android.AffirmConstants.PREQUAL_IS_SDK; +import static com.affirm.android.AffirmConstants.PREQUAL_PAGE_TYPE; +import static com.affirm.android.AffirmConstants.PREQUAL_PATH; +import static com.affirm.android.AffirmConstants.PREQUAL_PROMO_EXTERNAL_ID; +import static com.affirm.android.AffirmConstants.PREQUAL_PUBLIC_API_KEY; +import static com.affirm.android.AffirmConstants.PREQUAL_REFERRING_URL; +import static com.affirm.android.AffirmConstants.PREQUAL_UNIT_PRICE; +import static com.affirm.android.AffirmConstants.PREQUAL_USE_PROMO; +import static com.affirm.android.AffirmConstants.PROMO_ID; +import static com.affirm.android.AffirmConstants.REFERRING_URL; +import static com.affirm.android.AffirmTracker.TrackingEvent.PREQUAL_WEBVIEW_FAIL; +import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; +import static com.affirm.android.AffirmTracker.createTrackingException; + +public final class PrequalFragment extends AffirmFragment + implements PrequalWebViewClient.Callbacks { + + private static final String PREQUAL = "Prequal"; + private static final String TAG = TAG_PREFIX + "." + PREQUAL; + + private Affirm.PrequalCallbacks listener; + + private String amount; + private String promoId; + private String pageType; + + static PrequalFragment newInstance(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @NonNull BigDecimal amount, + @Nullable String promoId, + @Nullable String pageType) { + return newInstance(activity.getSupportFragmentManager(), containerViewId, amount, + promoId, pageType); + } + + static PrequalFragment newInstance(@NonNull Fragment fragment, + @IdRes int containerViewId, + @NonNull BigDecimal amount, + @Nullable String promoId, + @Nullable String pageType) { + return newInstance(fragment.getChildFragmentManager(), containerViewId, amount, + promoId, pageType); + } + + private static PrequalFragment newInstance(@NonNull FragmentManager fragmentManager, + @IdRes int containerViewId, + @NonNull BigDecimal amount, + @Nullable String promoId, + @Nullable String pageType) { + if (fragmentManager.findFragmentByTag(TAG) != null) { + return (PrequalFragment) fragmentManager.findFragmentByTag(TAG); + } + + String stringAmount = String.valueOf(AffirmUtils.decimalDollarsToIntegerCents(amount)); + PrequalFragment fragment = new PrequalFragment(); + Bundle bundle = new Bundle(); + bundle.putString(AMOUNT, stringAmount); + bundle.putString(PROMO_ID, promoId); + bundle.putString(PAGE_TYPE, pageType); + fragment.setArguments(bundle); + + addFragment(fragmentManager, containerViewId, fragment, TAG); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + amount = getArguments().getString(AMOUNT); + promoId = getArguments().getString(PROMO_ID); + pageType = getArguments().getString(PAGE_TYPE); + } + + @Override + void initViews() { + webView.setWebViewClient(new PrequalWebViewClient(this)); + webView.setWebChromeClient(new AffirmWebChromeClient(this)); + } + + @Override + void onAttached() { + String publicKey = AffirmPlugins.get().publicKey(); + String prequalUri = HTTPS_PROTOCOL + AffirmPlugins.get().basePromoUrl() + PREQUAL_PATH; + Uri.Builder builder = Uri.parse(prequalUri).buildUpon(); + builder.appendQueryParameter(PREQUAL_PUBLIC_API_KEY, publicKey); + builder.appendQueryParameter(PREQUAL_UNIT_PRICE, amount); + builder.appendQueryParameter(PREQUAL_USE_PROMO, "true"); + builder.appendQueryParameter(PREQUAL_IS_SDK, "true"); + builder.appendQueryParameter(PREQUAL_REFERRING_URL, REFERRING_URL); + if (promoId != null) { + builder.appendQueryParameter(PREQUAL_PROMO_EXTERNAL_ID, promoId); + } + if (pageType != null) { + builder.appendQueryParameter(PREQUAL_PAGE_TYPE, pageType); + } + webView.loadUrl(builder.build().toString()); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof Affirm.PrequalCallbacks) { + listener = (Affirm.PrequalCallbacks) context; + } else if (getParentFragment() instanceof Affirm.PrequalCallbacks) { + listener = (Affirm.PrequalCallbacks) getParentFragment(); + } + } + + @Override + public void onDetach() { + listener = null; + super.onDetach(); + } + + @Override + public void onWebViewError(@NonNull ConnectionException error) { + AffirmTracker.track(PREQUAL_WEBVIEW_FAIL, ERROR, createTrackingException(error)); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmPrequalError(error.toString()); + } + } + + @Override + public void onWebViewConfirmation() { + removeFragment(TAG); + if (getActivity() != null && getActivity() instanceof PrequalActivity) { + getActivity().finish(); + } + } +} \ No newline at end of file diff --git a/affirm/src/main/java/com/affirm/android/PromotionWebView.java b/affirm/src/main/java/com/affirm/android/PromotionWebView.java index 06e8ff58..37198d57 100644 --- a/affirm/src/main/java/com/affirm/android/PromotionWebView.java +++ b/affirm/src/main/java/com/affirm/android/PromotionWebView.java @@ -110,11 +110,14 @@ public void loadWebData(String promoHtml, String remoteCssUrl, String typeface) private String initialHtml(String promoHtml, String remoteCssUrl, String typeface) { String html; + InputStream ins = null; try { - final InputStream ins = getResources().openRawResource(R.raw.affirm_promo); + ins = getResources().openRawResource(R.raw.affirm_promo); html = AffirmUtils.readInputStream(ins); } catch (IOException e) { throw new RuntimeException(e); + } finally { + AffirmUtils.closeInputStream(ins); } final HashMap map = new HashMap<>(); diff --git a/affirm/src/main/java/com/affirm/android/VcnCheckoutActivity.java b/affirm/src/main/java/com/affirm/android/VcnCheckoutActivity.java index 5703da27..a8bf4f89 100755 --- a/affirm/src/main/java/com/affirm/android/VcnCheckoutActivity.java +++ b/affirm/src/main/java/com/affirm/android/VcnCheckoutActivity.java @@ -2,80 +2,50 @@ import android.app.Activity; import android.content.Intent; -import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import com.affirm.android.exception.AffirmException; -import com.affirm.android.exception.ConnectionException; import com.affirm.android.model.CardDetails; import com.affirm.android.model.CardDetailsInner; import com.affirm.android.model.Checkout; -import com.affirm.android.model.CheckoutResponse; import com.affirm.android.model.VcnReason; import org.joda.money.Money; -import java.io.IOException; -import java.io.InputStream; import java.util.Calendar; -import java.util.HashMap; -import static com.affirm.android.AffirmConstants.AFFIRM_CHECKOUT_CANCELLATION_URL; -import static com.affirm.android.AffirmConstants.AFFIRM_CHECKOUT_CONFIRMATION_URL; -import static com.affirm.android.AffirmConstants.CANCELLED_CB_URL; import static com.affirm.android.AffirmConstants.CHECKOUT_CAAS_EXTRA; import static com.affirm.android.AffirmConstants.CHECKOUT_CARD_AUTH_WINDOW; import static com.affirm.android.AffirmConstants.CHECKOUT_EXTRA; import static com.affirm.android.AffirmConstants.CHECKOUT_MONEY; -import static com.affirm.android.AffirmConstants.CONFIRM_CB_URL; -import static com.affirm.android.AffirmConstants.CREDIT_DETAILS; -import static com.affirm.android.AffirmConstants.HTTPS_PROTOCOL; import static com.affirm.android.AffirmConstants.NEW_FLOW; -import static com.affirm.android.AffirmConstants.TEXT_HTML; -import static com.affirm.android.AffirmConstants.URL; -import static com.affirm.android.AffirmConstants.URL2; -import static com.affirm.android.AffirmConstants.UTF_8; -import static com.affirm.android.AffirmConstants.VCN_REASON; -import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_CREATION_FAIL; -import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_CREATION_SUCCESS; -import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_WEBVIEW_FAIL; -import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_WEBVIEW_SUCCESS; -import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; -import static com.affirm.android.AffirmTracker.TrackingLevel.INFO; -import static com.affirm.android.AffirmTracker.createTrackingException; public class VcnCheckoutActivity extends CheckoutBaseActivity - implements VcnCheckoutWebViewClient.Callbacks { - - private static String receiveReasonCodes; + implements Affirm.VcnCheckoutCallbacks { static void startActivity(@NonNull Activity activity, int requestCode, @NonNull Checkout checkout, @Nullable String caas, - @Nullable Money money, int cardAuthWindow, @NonNull String configReceiveReasonCodes, + @Nullable Money money, int cardAuthWindow, boolean newFlow) { - Intent intent = buildIntent(activity, checkout, caas, money, cardAuthWindow, - configReceiveReasonCodes, newFlow); + Intent intent = buildIntent(activity, checkout, caas, money, cardAuthWindow, newFlow); startForResult(activity, intent, requestCode); } static void startActivity(@NonNull Fragment fragment, int requestCode, @NonNull Checkout checkout, @Nullable String caas, - @Nullable Money money, int cardAuthWindow, - @NonNull String configReceiveReasonCodes, boolean newFlow) { + @Nullable Money money, int cardAuthWindow, boolean newFlow) { Intent intent = buildIntent(fragment.requireActivity(), checkout, caas, money, - cardAuthWindow, configReceiveReasonCodes, newFlow); + cardAuthWindow, newFlow); startForResult(fragment, intent, requestCode); } private static Intent buildIntent( @NonNull Activity originalActivity, @NonNull Checkout checkout, @Nullable String caas, @Nullable Money money, - int cardAuthWindow, @NonNull String configReceiveReasonCodes, boolean newFlow) { + int cardAuthWindow, boolean newFlow) { - receiveReasonCodes = configReceiveReasonCodes; final Intent intent = new Intent(originalActivity, VcnCheckoutActivity.class); intent.putExtra(CHECKOUT_EXTRA, checkout); intent.putExtra(CHECKOUT_CAAS_EXTRA, caas); @@ -85,61 +55,28 @@ private static Intent buildIntent( return intent; } - @Override - void initViews() { - AffirmUtils.debuggableWebView(this); - webView.setWebViewClient( - new VcnCheckoutWebViewClient(AffirmPlugins.get().gson(), receiveReasonCodes, this)); - webView.setWebChromeClient(new AffirmWebChromeClient(this)); - } - @Override boolean useVCN() { return true; } @Override - InnerCheckoutCallback getInnerCheckoutCallback() { - return new InnerCheckoutCallback() { - @Override - public void onError(@NonNull AffirmException exception) { - AffirmTracker.track(VCN_CHECKOUT_CREATION_FAIL, ERROR, - createTrackingException(exception)); - finishWithError(exception); - } - - @Override - public void onSuccess(@NonNull CheckoutResponse response) { - AffirmTracker.track(VCN_CHECKOUT_CREATION_SUCCESS, INFO, null); - final String html = initialHtml(response); - final Uri uri = Uri.parse(response.redirectUrl()); - webView.loadDataWithBaseURL(HTTPS_PROTOCOL + uri.getHost(), html, - TEXT_HTML, UTF_8, null); - } - }; + public void onAffirmVcnCheckoutError(@Nullable String message) { + finishWithError(message); } - private String initialHtml(@NonNull CheckoutResponse response) { - String html; - try { - final InputStream ins = getResources().openRawResource(R.raw.affirm_vcn_checkout); - html = AffirmUtils.readInputStream(ins); - } catch (IOException e) { - throw new RuntimeException(e); - } - - final HashMap map = new HashMap<>(); + @Override + public void onAffirmVcnCheckoutCancelled() { + finishWithCancellation(); + } - map.put(URL, response.redirectUrl()); - map.put(URL2, response.redirectUrl()); - map.put(CONFIRM_CB_URL, AFFIRM_CHECKOUT_CONFIRMATION_URL); - map.put(CANCELLED_CB_URL, AFFIRM_CHECKOUT_CANCELLATION_URL); - return AffirmUtils.replacePlaceholders(html, map); + @Override + public void onAffirmVcnCheckoutCancelledReason(@NonNull VcnReason vcnReason) { + finishWithVcnReason(vcnReason); } @Override - public void onWebViewConfirmation(@NonNull CardDetails cardDetails) { - AffirmTracker.track(VCN_CHECKOUT_WEBVIEW_SUCCESS, INFO, null); + public void onAffirmVcnCheckoutSuccess(@NonNull CardDetails cardDetails) { if (newFlow) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.HOUR, 24); @@ -147,33 +84,10 @@ public void onWebViewConfirmation(@NonNull CardDetails cardDetails) { new CardDetailsInner(cardDetails, calendar.getTime())); Affirm.startVcnDisplay(this, checkout, caas); } else { - final Intent intent = new Intent(); - intent.putExtra(CREDIT_DETAILS, cardDetails); - setResult(RESULT_OK, intent); - finish(); + finishWithCardDetail(cardDetails); } } - @Override - public void onWebViewError(@NonNull ConnectionException error) { - AffirmTracker.track(VCN_CHECKOUT_WEBVIEW_FAIL, ERROR, createTrackingException(error)); - - finishWithError(error); - } - - @Override - public void onWebViewCancellation() { - webViewCancellation(); - } - - @Override - public void onWebViewCancellationReason(@NonNull VcnReason vcnReason) { - final Intent intent = new Intent(); - intent.putExtra(VCN_REASON, vcnReason); - setResult(RESULT_CANCELED, intent); - finish(); - } - @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/affirm/src/main/java/com/affirm/android/VcnCheckoutFragment.java b/affirm/src/main/java/com/affirm/android/VcnCheckoutFragment.java new file mode 100644 index 00000000..49cd0f8b --- /dev/null +++ b/affirm/src/main/java/com/affirm/android/VcnCheckoutFragment.java @@ -0,0 +1,224 @@ +package com.affirm.android; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.affirm.android.exception.AffirmException; +import com.affirm.android.exception.ConnectionException; +import com.affirm.android.model.CardDetails; +import com.affirm.android.model.Checkout; +import com.affirm.android.model.CheckoutResponse; +import com.affirm.android.model.VcnReason; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; + +import static com.affirm.android.AffirmConstants.AFFIRM_CHECKOUT_CANCELLATION_URL; +import static com.affirm.android.AffirmConstants.AFFIRM_CHECKOUT_CONFIRMATION_URL; +import static com.affirm.android.AffirmConstants.CANCELLED_CB_URL; +import static com.affirm.android.AffirmConstants.CHECKOUT_CAAS_EXTRA; +import static com.affirm.android.AffirmConstants.CHECKOUT_CARD_AUTH_WINDOW; +import static com.affirm.android.AffirmConstants.CHECKOUT_EXTRA; +import static com.affirm.android.AffirmConstants.CHECKOUT_MONEY; +import static com.affirm.android.AffirmConstants.CHECKOUT_RECEIVE_REASON_CODES; +import static com.affirm.android.AffirmConstants.CONFIRM_CB_URL; +import static com.affirm.android.AffirmConstants.HTTPS_PROTOCOL; +import static com.affirm.android.AffirmConstants.NEW_FLOW; +import static com.affirm.android.AffirmConstants.TEXT_HTML; +import static com.affirm.android.AffirmConstants.URL; +import static com.affirm.android.AffirmConstants.URL2; +import static com.affirm.android.AffirmConstants.UTF_8; +import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_CREATION_FAIL; +import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_CREATION_SUCCESS; +import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_WEBVIEW_FAIL; +import static com.affirm.android.AffirmTracker.TrackingEvent.VCN_CHECKOUT_WEBVIEW_SUCCESS; +import static com.affirm.android.AffirmTracker.TrackingLevel.ERROR; +import static com.affirm.android.AffirmTracker.TrackingLevel.INFO; +import static com.affirm.android.AffirmTracker.createTrackingException; + +import org.joda.money.Money; + +public final class VcnCheckoutFragment extends CheckoutBaseFragment + implements VcnCheckoutWebViewClient.Callbacks { + + private static final String VCN_CHECKOUT = "VCN_Checkout"; + private static final String TAG = TAG_PREFIX + "." + VCN_CHECKOUT; + + private String receiveReasonCodes; + + private Affirm.VcnCheckoutCallbacks listener; + + static VcnCheckoutFragment newInstance(@NonNull AppCompatActivity activity, + @IdRes int containerViewId, + @NonNull Checkout checkout, + String receiveReasonCodes, + @Nullable String caas, + @Nullable Money money, + int cardAuthWindow, + boolean newFlow) { + return newInstance(activity.getSupportFragmentManager(), containerViewId, checkout, + receiveReasonCodes, caas, money, cardAuthWindow, newFlow); + } + + static VcnCheckoutFragment newInstance(@NonNull Fragment fragment, + @IdRes int containerViewId, + @NonNull Checkout checkout, + String receiveReasonCodes, + @Nullable String caas, + @Nullable Money money, + int cardAuthWindow, + boolean newFlow) { + return newInstance(fragment.getChildFragmentManager(), containerViewId, checkout, + receiveReasonCodes, caas, money, cardAuthWindow, newFlow); + } + + private static VcnCheckoutFragment newInstance(@NonNull FragmentManager fragmentManager, + @IdRes int containerViewId, + @NonNull Checkout checkout, + String receiveReasonCodes, + @Nullable String caas, + @Nullable Money money, + int cardAuthWindow, + boolean newFlow) { + if (fragmentManager.findFragmentByTag(TAG) != null) { + return (VcnCheckoutFragment) fragmentManager.findFragmentByTag(TAG); + } + + VcnCheckoutFragment fragment = new VcnCheckoutFragment(); + Bundle bundle = new Bundle(); + bundle.putString(CHECKOUT_RECEIVE_REASON_CODES, receiveReasonCodes); + bundle.putParcelable(CHECKOUT_EXTRA, checkout); + bundle.putString(CHECKOUT_CAAS_EXTRA, caas); + bundle.putSerializable(CHECKOUT_MONEY, money); + bundle.putInt(CHECKOUT_CARD_AUTH_WINDOW, cardAuthWindow); + bundle.putBoolean(NEW_FLOW, newFlow); + fragment.setArguments(bundle); + + addFragment(fragmentManager, containerViewId, fragment, TAG); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + receiveReasonCodes = getArguments().getString(CHECKOUT_RECEIVE_REASON_CODES); + } + + @Override + void initViews() { + webView.setWebViewClient(new VcnCheckoutWebViewClient( + AffirmPlugins.get().gson(), receiveReasonCodes, this)); + webView.setWebChromeClient(new AffirmWebChromeClient(this)); + } + + @Override + boolean useVCN() { + return true; + } + + @Override + InnerCheckoutCallback innerCheckoutCallback() { + return new InnerCheckoutCallback() { + @Override + public void onError(@NonNull AffirmException exception) { + AffirmTracker.track(VCN_CHECKOUT_CREATION_FAIL, ERROR, + createTrackingException(exception)); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmVcnCheckoutError(exception.toString()); + } + } + + @Override + public void onSuccess(@NonNull CheckoutResponse response) { + AffirmTracker.track(VCN_CHECKOUT_CREATION_SUCCESS, INFO, null); + final String html = initialHtml(response); + final Uri uri = Uri.parse(response.redirectUrl()); + webView.loadDataWithBaseURL(HTTPS_PROTOCOL + uri.getHost(), html, + TEXT_HTML, UTF_8, null); + } + }; + } + + private String initialHtml(@NonNull CheckoutResponse response) { + String html; + InputStream ins = null; + try { + ins = getResources().openRawResource(R.raw.affirm_vcn_checkout); + html = AffirmUtils.readInputStream(ins); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + AffirmUtils.closeInputStream(ins); + } + + final HashMap map = new HashMap<>(); + + map.put(URL, response.redirectUrl()); + map.put(URL2, response.redirectUrl()); + map.put(CONFIRM_CB_URL, AFFIRM_CHECKOUT_CONFIRMATION_URL); + map.put(CANCELLED_CB_URL, AFFIRM_CHECKOUT_CANCELLATION_URL); + return AffirmUtils.replacePlaceholders(html, map); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof Affirm.VcnCheckoutCallbacks) { + listener = (Affirm.VcnCheckoutCallbacks) context; + } else if (getParentFragment() instanceof Affirm.VcnCheckoutCallbacks) { + listener = (Affirm.VcnCheckoutCallbacks) getParentFragment(); + } + } + + @Override + public void onDetach() { + listener = null; + super.onDetach(); + } + + @Override + public void onWebViewConfirmation(@NonNull CardDetails cardDetails) { + AffirmTracker.track(VCN_CHECKOUT_WEBVIEW_SUCCESS, INFO, null); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmVcnCheckoutSuccess(cardDetails); + } + } + + @Override + public void onWebViewError(@NonNull ConnectionException error) { + AffirmTracker.track(VCN_CHECKOUT_WEBVIEW_FAIL, ERROR, + createTrackingException(error)); + removeFragment(TAG); + if (listener != null) { + listener.onAffirmVcnCheckoutError(error.toString()); + } + } + + @Override + public void onWebViewCancellation() { + removeFragment(TAG); + if (listener != null) { + listener.onAffirmVcnCheckoutCancelled(); + } + } + + @Override + public void onWebViewCancellationReason(@NonNull VcnReason vcnReason) { + removeFragment(TAG); + if (listener != null) { + listener.onAffirmVcnCheckoutCancelledReason(vcnReason); + } + } +} \ No newline at end of file diff --git a/affirm/src/main/res/layout/affirm_activity_webview.xml b/affirm/src/main/res/layout/affirm_fragment_webview.xml similarity index 80% rename from affirm/src/main/res/layout/affirm_activity_webview.xml rename to affirm/src/main/res/layout/affirm_fragment_webview.xml index 432486e1..4a823c1b 100755 --- a/affirm/src/main/res/layout/affirm_activity_webview.xml +++ b/affirm/src/main/res/layout/affirm_fragment_webview.xml @@ -1,12 +1,11 @@ - + android:layout_height="match_parent" /> + android:indeterminate="true" /> - \ No newline at end of file + \ No newline at end of file diff --git a/samples-java/src/androidTest/java/com/affirm/samples/MainActivityEspressoTest.java b/samples-java/src/androidTest/java/com/affirm/samples/MainActivityEspressoTest.java index e63552d6..fa50ae7a 100755 --- a/samples-java/src/androidTest/java/com/affirm/samples/MainActivityEspressoTest.java +++ b/samples-java/src/androidTest/java/com/affirm/samples/MainActivityEspressoTest.java @@ -452,4 +452,212 @@ public void testProductModal() throws Exception { pressBack(); } + + @Test + public void testFragmentCheckout() throws Exception { + final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + onView(withId(R.id.clearCookies)).perform(click()); + + SystemClock.sleep(4000); + + // Review plan + new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(1); + + SystemClock.sleep(4000); + + onView(withId(R.id.fragmentUsages)).perform(click()); + + SystemClock.sleep(4000); + + final int timeOut = 1000 * 60; + + onView(withId(R.id.checkout)).perform(click()); + device.wait(Until.findObject(By.clazz(WebView.class)), timeOut); + + // Input number + UiObject emailInput = device.findObject(new UiSelector() + .instance(0) + .className(EditText.class)); + + emailInput.waitForExists(timeOut); + emailInput.setText("8888888888"); + + new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(1); + + SystemClock.sleep(4000); + + UiObject buttonContinue = device.findObject(new UiSelector() + .instance(5) + .className(Button.class)); + + buttonContinue.waitForExists(timeOut); + buttonContinue.clickAndWaitForNewWindow(); + + // Input code + UiObject inputValidate = device.findObject(new UiSelector() + .instance(0) + .className(EditText.class)); + + inputValidate.waitForExists(timeOut); + inputValidate.setText("1234"); + + SystemClock.sleep(2000); + + // Select payment plan + UiObject appItem = device.findObject(new UiSelector().className("android.widget.ListView").instance(0) + .childSelector(new UiSelector().className(View.class).instance(0)) + ); + + appItem.waitForExists(timeOut); + appItem.clickAndWaitForNewWindow(); + + SystemClock.sleep(2000); + + // Review plan + UiScrollable appView = new UiScrollable(new UiSelector().scrollable(true)); + + appView.scrollToEnd(1); + + device.findObject(new UiSelector().text("Continue").className(Button.class)).clickAndWaitForNewWindow(); + + SystemClock.sleep(2000); + + // Confirm + appView.scrollIntoView(new UiSelector().className(Button.class).instance(7)); + + SystemClock.sleep(2000); + + appView.scrollToEnd(1); + + UiObject checkbox = device.findObject(new UiSelector().className(CheckBox.class).instance(0)); + + int w = device.getDisplayWidth(); + device.click(w / 2, checkbox.getVisibleBounds().centerY() + 30); + +// checkbox.waitForExists(timeOut); +// checkbox.clickAndWaitForNewWindow(); + + SystemClock.sleep(2000); + + device.findObject(new UiSelector().resourceId("confirm-submit").className(Button.class)).clickAndWaitForNewWindow(); + + SystemClock.sleep(10000); + } + + @Test + public void testFragmentVCNCheckout() throws Exception { + final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + onView(withId(R.id.clearCookies)).perform(click()); + + SystemClock.sleep(4000); + + // Review plan + new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(1); + + SystemClock.sleep(4000); + + onView(withId(R.id.fragmentUsages)).perform(click()); + + SystemClock.sleep(4000); + + final int timeOut = 1000 * 60; + onView(withId(R.id.vcnCheckout)).perform(click()); + + device.wait(Until.findObject(By.clazz(WebView.class)), timeOut); + + // Input number + UiObject emailInput = device.findObject(new UiSelector() + .instance(0) + .className(EditText.class)); + + emailInput.waitForExists(timeOut); + emailInput.setText("8888888888"); + + new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(1); + + SystemClock.sleep(4000); + + UiObject buttonContinue = device.findObject(new UiSelector() + .instance(5) + .className(Button.class)); + + buttonContinue.waitForExists(timeOut); + buttonContinue.clickAndWaitForNewWindow(); + + new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(1); + + // Input code + UiObject inputValidate = device.findObject(new UiSelector() + .instance(0) + .className(EditText.class)); + + inputValidate.waitForExists(timeOut); + inputValidate.setText("1234"); + + SystemClock.sleep(2000); + + // Select payment plan + UiObject appItem = device.findObject(new UiSelector().className("android.widget.ListView").instance(0) + .childSelector(new UiSelector().className(View.class).instance(0)) + ); + + appItem.waitForExists(timeOut); + appItem.clickAndWaitForNewWindow(); + + SystemClock.sleep(2000); + + // Review plan + UiScrollable appView = new UiScrollable(new UiSelector().scrollable(true)); +// +// appView.scrollToEnd(1); +// +// SystemClock.sleep(2000); + + int w = device.getDisplayWidth(); + int h = device.getDisplayHeight(); + device.click(w / 2, h - 200); + + // The uiautomator map doesn't have the right coordinates after scroll to bottom +// UiObject continueButton = device.findObject(new UiSelector().text("Continue").className(Button.class)); +// +// continueButton.waitForExists(timeOut); +// continueButton.clickAndWaitForNewWindow(); + + SystemClock.sleep(2000); + + appView.scrollToEnd(1); + + device.findObject(new UiSelector().text("Continue").className(Button.class)).clickAndWaitForNewWindow(); + + SystemClock.sleep(2000); + + appView.scrollIntoView(new UiSelector().className(Button.class).instance(10)); + + SystemClock.sleep(2000); + + appView.scrollToEnd(1); + + // Confirm + UiObject checkbox = device.findObject(new UiSelector().className(CheckBox.class).instance(0)); + + device.click(device.getDisplayWidth() / 2, checkbox.getVisibleBounds().centerY() + 30); + +// checkbox.waitForExists(timeOut); +// checkbox.clickAndWaitForNewWindow(); + + SystemClock.sleep(2000); + + device.findObject(new UiSelector().resourceId("confirm-submit").className(Button.class)).clickAndWaitForNewWindow(); + +// new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().className(Button.class).instance(9)); + + // The uiautomator map doesn't have the right coordinates after scroll to bottom +// device.findObject(new UiSelector().resourceId("confirm-submit").className(Button.class)).clickAndWaitForNewWindow(); + +// device.click(w / 2, h - 200); + + SystemClock.sleep(10000); + } } diff --git a/samples-java/src/main/AndroidManifest.xml b/samples-java/src/main/AndroidManifest.xml index cda671b5..b96c94f3 100755 --- a/samples-java/src/main/AndroidManifest.xml +++ b/samples-java/src/main/AndroidManifest.xml @@ -23,6 +23,14 @@ + + + \ No newline at end of file diff --git a/samples-java/src/main/java/com/affirm/samples/Config.java b/samples-java/src/main/java/com/affirm/samples/Config.java new file mode 100644 index 00000000..ed617e7a --- /dev/null +++ b/samples-java/src/main/java/com/affirm/samples/Config.java @@ -0,0 +1,8 @@ +package com.affirm.samples; + +public class Config { + + public static final String PUBLIC_KEY = "Y8CQXFF044903JC0"; + + public static final String MODAL_ID = "5LNMQ33SEUYHLNUC"; +} \ No newline at end of file diff --git a/samples-java/src/main/java/com/affirm/samples/FragmentUsagesActivity.java b/samples-java/src/main/java/com/affirm/samples/FragmentUsagesActivity.java new file mode 100644 index 00000000..17533a6f --- /dev/null +++ b/samples-java/src/main/java/com/affirm/samples/FragmentUsagesActivity.java @@ -0,0 +1,14 @@ +package com.affirm.samples; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +public class FragmentUsagesActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_fragment_usages); + } +} \ No newline at end of file diff --git a/samples-java/src/main/java/com/affirm/samples/FragmentUsagesFragment.java b/samples-java/src/main/java/com/affirm/samples/FragmentUsagesFragment.java new file mode 100644 index 00000000..507c2995 --- /dev/null +++ b/samples-java/src/main/java/com/affirm/samples/FragmentUsagesFragment.java @@ -0,0 +1,169 @@ +package com.affirm.samples; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; + +import com.affirm.android.Affirm; +import com.affirm.android.AffirmPromotionButton; +import com.affirm.android.model.Address; +import com.affirm.android.model.Billing; +import com.affirm.android.model.CardDetails; +import com.affirm.android.model.Checkout; +import com.affirm.android.model.Currency; +import com.affirm.android.model.Item; +import com.affirm.android.model.Name; +import com.affirm.android.model.PromoPageType; +import com.affirm.android.model.Shipping; +import com.affirm.android.model.VcnReason; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +public class FragmentUsagesFragment extends Fragment implements Affirm.CheckoutCallbacks, Affirm.VcnCheckoutCallbacks, Affirm.PrequalCallbacks { + + private static final BigDecimal PRICE = BigDecimal.valueOf(1100.0); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_fragment_usages, container); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + AffirmPromotionButton affirmPromotionButton = view.findViewById(R.id.promo); + Affirm.configureWithAmount(this, R.id.container, affirmPromotionButton, null, PromoPageType.PRODUCT, PRICE, true, null); + + view.findViewById(R.id.checkout).setOnClickListener(v -> { + try { + Affirm.startCheckout(this, R.id.container, checkoutModel(), null, 10, false); + } catch (Exception e) { + Toast.makeText(getContext(), "Checkout failed, reason: " + e.toString(), Toast.LENGTH_SHORT).show(); + } + }); + + view.findViewById(R.id.vcnCheckout).setOnClickListener(v -> { + try { + Affirm.startCheckout(this, R.id.container, checkoutModel(), null, 10, true); + } catch (Exception e) { + Toast.makeText(getContext(), "VCN Checkout failed, reason: " + e.toString(), Toast.LENGTH_SHORT).show(); + } + }); + + view.findViewById(R.id.siteModalButton).setOnClickListener(v -> Affirm.showSiteModal(this, R.id.container, null, Config.MODAL_ID)); + + view.findViewById(R.id.productModalButton).setOnClickListener(v -> Affirm.showProductModal(this, R.id.container, PRICE, null, PromoPageType.PRODUCT, null)); + } + + private Checkout checkoutModel() { + final Item item = Item.builder() + .setDisplayName("Great Deal Wheel") + .setImageUrl( + "http://www.m2motorsportinc.com/media/catalog/product/cache/1/thumbnail" + + "/9df78eab33525d08d6e5fb8d27136e95/v/e/velocity-vw125-wheels-rims.jpg") + .setQty(1) + .setSku("wheel") + .setUnitPrice(BigDecimal.valueOf(1000.0)) + .setUrl("http://merchant.com/great_deal_wheel") + .build(); + + final Map items = new HashMap<>(); + items.put("wheel", item); + + final Name name = Name.builder().setFull("John Smith").build(); + + // In US, use Address + final Address address = Address.builder() + .setCity("San Francisco") + .setCountry("USA") + .setLine1("333 Kansas st") + .setState("CA") + .setZipcode("94107") + .build(); + + // In canadian, use CAAddress +// final AbstractAddress address = CAAddress.builder() +// .setStreet1("123 Alder Creek Dr.") +// .setStreet2("Floor 7") +// .setCity("Toronto") +// .setRegion1Code("ON") +// .setPostalCode("M4B 1B3") +// .setCountryCode("CA") +// .build(); + + final Shipping shipping = Shipping.builder().setAddress(address).setName(name).build(); + final Billing billing = Billing.builder().setAddress(address).setName(name).build(); + + // More details on https://docs.affirm.com/affirm-developers/reference/the-metadata-object + final Map metadata = new HashMap<>(); + metadata.put("webhook_session_id", "ABC123"); + metadata.put("shipping_type", "UPS Ground"); + metadata.put("entity_name", "internal-sub_brand-name"); + + return Checkout.builder() + .setOrderId("55555") + .setItems(items) + .setBilling(billing) + .setShipping(shipping) + .setShippingAmount(BigDecimal.valueOf(0.0)) + .setTaxAmount(BigDecimal.valueOf(100.0)) + .setTotal(PRICE) + .setCurrency(Currency.USD) // For Canadian, you must set "CAD"; For American, this is optional, you can set "USD" or not set. + .setMetadata(metadata) + .build(); + } + + // - Affirm.CheckoutCallbacks + @Override + public void onAffirmCheckoutSuccess(@NonNull String token) { + Toast.makeText(getContext(), "Checkout token: " + token, Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmCheckoutCancelled() { + Toast.makeText(getContext(), "Checkout Cancelled", Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmCheckoutError(@Nullable String message) { + Toast.makeText(getContext(), "Checkout Error: " + message, Toast.LENGTH_LONG).show(); + } + + // - Affirm.VcnCheckoutCallbacks + @Override + public void onAffirmVcnCheckoutCancelled() { + Toast.makeText(getContext(), "Vcn Checkout Cancelled", Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmVcnCheckoutCancelledReason(@NonNull VcnReason vcnReason) { + Toast.makeText(getContext(), "Vcn Checkout Cancelled: " + vcnReason.toString(), Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmVcnCheckoutError(@Nullable String message) { + Toast.makeText(getContext(), "Vcn Checkout Error: " + message, Toast.LENGTH_LONG).show(); + } + + @Override + public void onAffirmVcnCheckoutSuccess(@NonNull CardDetails cardDetails) { + Toast.makeText(getContext(), "Vcn Checkout Card: " + cardDetails.toString(), Toast.LENGTH_LONG).show(); + } + + // - Prequal + @Override + public void onAffirmPrequalError(@Nullable String message) { + Toast.makeText(getContext(), "Prequal Error: " + message, Toast.LENGTH_LONG).show(); + } +} \ No newline at end of file diff --git a/samples-java/src/main/java/com/affirm/samples/MainFragment.java b/samples-java/src/main/java/com/affirm/samples/MainFragment.java index 5807cbb9..72218a18 100644 --- a/samples-java/src/main/java/com/affirm/samples/MainFragment.java +++ b/samples-java/src/main/java/com/affirm/samples/MainFragment.java @@ -127,7 +127,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat return false; }); - view.findViewById(R.id.siteModalButton).setOnClickListener(v -> Affirm.showSiteModal(MainFragment.this, "5LNMQ33SEUYHLNUC")); + view.findViewById(R.id.siteModalButton).setOnClickListener(v -> Affirm.showSiteModal(MainFragment.this, Config.MODAL_ID)); view.findViewById(R.id.productModalButton).setOnClickListener(v -> Affirm.showProductModal(MainFragment.this, PRICE, null, PromoPageType.PRODUCT, null)); @@ -138,6 +138,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat view.findViewById(R.id.clearCookies).setOnClickListener(v -> CookiesUtil.clearCookies(getContext())); + view.findViewById(R.id.fragmentUsages).setOnClickListener(v -> startActivity(new Intent(getActivity(), FragmentUsagesActivity.class))); + // Option1 - Load via findViewById AffirmPromotionButton affirmPromotionButton1 = view.findViewById(R.id.promo); // Default use Button to show the promo message with configWithLocalStyling. If you want to use WebView to show the promo message. You should use configWithHtmlStyling diff --git a/samples-java/src/main/java/com/affirm/samples/SampleApplication.java b/samples-java/src/main/java/com/affirm/samples/SampleApplication.java index 9ca0a831..8f893c08 100755 --- a/samples-java/src/main/java/com/affirm/samples/SampleApplication.java +++ b/samples-java/src/main/java/com/affirm/samples/SampleApplication.java @@ -10,7 +10,7 @@ public class SampleApplication extends Application { public void onCreate() { super.onCreate(); - Affirm.initialize(new Affirm.Configuration.Builder("6YN1HYTDBEMFF8CK") // In Canadian, should use Canada public API key + Affirm.initialize(new Affirm.Configuration.Builder(Config.PUBLIC_KEY) // In Canadian, should use Canada public API key .setEnvironment(Affirm.Environment.SANDBOX) .setMerchantName(null) .setReceiveReasonCodes("true") diff --git a/samples-java/src/main/res/layout/activity_fragment_usages.xml b/samples-java/src/main/res/layout/activity_fragment_usages.xml new file mode 100755 index 00000000..e84c43f5 --- /dev/null +++ b/samples-java/src/main/res/layout/activity_fragment_usages.xml @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/samples-java/src/main/res/layout/fragment_fragment_usages.xml b/samples-java/src/main/res/layout/fragment_fragment_usages.xml new file mode 100755 index 00000000..795663f8 --- /dev/null +++ b/samples-java/src/main/res/layout/fragment_fragment_usages.xml @@ -0,0 +1,66 @@ + + + + + + + +