diff --git a/CHANGELOG.md b/CHANGELOG.md index a369268fd..8eba1767a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Braintree Android Drop-In Release Notes +## unreleased + +* Breaking Changes + * Remove `DropInClient` and replace it with `DropInLauncher` + * Add `DropInLauncherCallback` to receive a result from `DropInActivity` via the Activity Result API + * Create `RecentPaymentMethodsClient` with `fetchMostRecentPaymentMethod()` + ## 6.15.0 * Refresh vaulted payment methods list after 3DS is canceled (fixes #455) diff --git a/Demo/src/main/java/com/braintreepayments/demo/BaseActivity.java b/Demo/src/main/java/com/braintreepayments/demo/BaseActivity.java index 952e6c5d0..420d74200 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/BaseActivity.java +++ b/Demo/src/main/java/com/braintreepayments/demo/BaseActivity.java @@ -5,8 +5,6 @@ import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.ArrayAdapter; @@ -16,13 +14,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback; -import com.braintreepayments.api.DropInClient; -import com.braintreepayments.demo.models.ClientToken; - -import retrofit.Callback; -import retrofit.RetrofitError; -import retrofit.client.Response; - @SuppressWarnings("deprecation") public abstract class BaseActivity extends AppCompatActivity implements OnRequestPermissionsResultCallback, ActionBar.OnNavigationListener { diff --git a/Demo/src/main/java/com/braintreepayments/demo/MainActivity.java b/Demo/src/main/java/com/braintreepayments/demo/MainActivity.java index 2e226de98..d65ae10ac 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/MainActivity.java +++ b/Demo/src/main/java/com/braintreepayments/demo/MainActivity.java @@ -3,6 +3,7 @@ import static android.view.View.GONE; import static android.view.View.VISIBLE; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -16,7 +17,8 @@ import androidx.cardview.widget.CardView; import com.braintreepayments.api.CardNonce; -import com.braintreepayments.api.DropInClient; +import com.braintreepayments.api.ClientTokenCallback; +import com.braintreepayments.api.DropInLauncher; import com.braintreepayments.api.DropInListener; import com.braintreepayments.api.DropInPaymentMethod; import com.braintreepayments.api.DropInRequest; @@ -26,6 +28,7 @@ import com.braintreepayments.api.PayPalAccountNonce; import com.braintreepayments.api.PaymentMethodNonce; import com.braintreepayments.api.PostalAddress; +import com.braintreepayments.api.RecentPaymentMethodsClient; import com.braintreepayments.api.ThreeDSecureAdditionalInformation; import com.braintreepayments.api.ThreeDSecurePostalAddress; import com.braintreepayments.api.ThreeDSecureRequest; @@ -36,6 +39,8 @@ import com.google.android.gms.wallet.TransactionInfo; import com.google.android.gms.wallet.WalletConstants; +import java.lang.ref.WeakReference; + public class MainActivity extends BaseActivity implements DropInListener { private static final String KEY_NONCE = "nonce"; @@ -53,17 +58,32 @@ public class MainActivity extends BaseActivity implements DropInListener { private Button addPaymentMethodButton; private Button purchaseButton; - private DropInClient dropInClient; + private RecentPaymentMethodsClient recentPaymentMethodsClient; private boolean purchased = false; private SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener; + private DemoClientTokenProvider clientTokenProvider; + + private String authString; + + private final DropInLauncher dropInLauncher = new DropInLauncher(this, (dropInResult) -> { + Exception error = dropInResult.getError(); + if (error != null) { + onDropInFailure(error); + } else { + onDropInSuccess(dropInResult); + } + }); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); + clientTokenProvider = new DemoClientTokenProvider(this); + paymentMethod = findViewById(R.id.payment_method); paymentMethodIcon = findViewById(R.id.payment_method_icon); paymentMethodTitle = findViewById(R.id.payment_method_title); @@ -120,18 +140,33 @@ private void registerSharedPreferencesListener() { private void configureDropInClient() { if (Settings.useTokenizationKey(this)) { - String tokenizationKey = Settings.getEnvironmentTokenizationKey(this); - dropInClient = new DropInClient(this, tokenizationKey); - dropInClient.setListener(this); + authString = Settings.getEnvironmentTokenizationKey(this); addPaymentMethodButton.setVisibility(VISIBLE); } else { - dropInClient = new DropInClient(this, new DemoClientTokenProvider(this)); - dropInClient.setListener(this); - dropInClient.fetchMostRecentPaymentMethod(this, (dropInResult, error) -> { - if (dropInResult != null) { - handleDropInResult(dropInResult); - } else { - addPaymentMethodButton.setVisibility(VISIBLE); + Context appContext = getApplicationContext(); + WeakReference activityRef = new WeakReference<>(this); + clientTokenProvider.getClientToken(new ClientTokenCallback() { + @Override + public void onSuccess(@NonNull String clientToken) { + MainActivity activity = activityRef.get(); + if (activity != null) { + authString = clientToken; + recentPaymentMethodsClient = new RecentPaymentMethodsClient(appContext, clientToken); + activity.addPaymentMethodButton.setVisibility(VISIBLE); + + recentPaymentMethodsClient.fetchMostRecentPaymentMethod(activity, (dropInResult, error) -> { + if (dropInResult != null) { + handleDropInResult(dropInResult); + } else { + addPaymentMethodButton.setVisibility(VISIBLE); + } + }); + } + } + + @Override + public void onFailure(@NonNull Exception e) { + onDropInFailure(e); } }); } @@ -152,7 +187,8 @@ public void launchDropIn(View v) { dropInRequest.setThreeDSecureRequest(demoThreeDSecureRequest()); } - dropInClient.launchDropIn(dropInRequest); + // TODO: make auth string a required DropInRequest constructor parameter + dropInLauncher.start(authString, dropInRequest); } private ThreeDSecureRequest demoThreeDSecureRequest() { @@ -191,7 +227,7 @@ public void purchase(View v) { public void handleDropInResult(DropInResult result) { if (result.getPaymentMethodType() == null - || result.getPaymentMethodType() == DropInPaymentMethod.GOOGLE_PAY) { + || result.getPaymentMethodType() == DropInPaymentMethod.GOOGLE_PAY) { // google pay doesn't have a payment method nonce to display; fallback to OG ui addPaymentMethodButton.setVisibility(VISIBLE); } else { diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java index c42e71155..b720f6335 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInActivity.java @@ -1,5 +1,10 @@ package com.braintreepayments.api; +import static com.braintreepayments.api.DropInLauncher.EXTRA_AUTHORIZATION; +import static com.braintreepayments.api.DropInLauncher.EXTRA_AUTHORIZATION_ERROR; +import static com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST; +import static com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST_BUNDLE; + import android.content.Intent; import android.os.Bundle; @@ -65,7 +70,7 @@ protected void onCreate(Bundle savedInstanceState) { Intent intent = getIntent(); Exception error = - (Exception) intent.getSerializableExtra(DropInClient.EXTRA_AUTHORIZATION_ERROR); + (Exception) intent.getSerializableExtra(EXTRA_AUTHORIZATION_ERROR); if (error != null) { // echo back error to merchant via activity result finishDropInWithError(error); @@ -73,10 +78,9 @@ protected void onCreate(Bundle savedInstanceState) { } if (dropInInternalClient == null) { - String authorization = intent.getStringExtra(DropInClient.EXTRA_AUTHORIZATION); - String sessionId = intent.getStringExtra(DropInClient.EXTRA_SESSION_ID); + String authorization = intent.getStringExtra(EXTRA_AUTHORIZATION); DropInRequest dropInRequest = getDropInRequest(intent); - dropInInternalClient = new DropInInternalClient(this, authorization, sessionId, dropInRequest); + dropInInternalClient = new DropInInternalClient(this, authorization, dropInRequest); } alertPresenter = new AlertPresenter(); @@ -120,9 +124,9 @@ void finishDropInWithError(Exception e) { } private DropInRequest getDropInRequest(Intent intent) { - Bundle bundle = intent.getParcelableExtra(DropInClient.EXTRA_CHECKOUT_REQUEST_BUNDLE); + Bundle bundle = intent.getParcelableExtra(EXTRA_CHECKOUT_REQUEST_BUNDLE); bundle.setClassLoader(DropInRequest.class.getClassLoader()); - return bundle.getParcelable(DropInClient.EXTRA_CHECKOUT_REQUEST); + return bundle.getParcelable(EXTRA_CHECKOUT_REQUEST); } @VisibleForTesting diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInActivityResultContract.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInActivityResultContract.java index 881ac99bc..6bea3f21a 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInActivityResultContract.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInActivityResultContract.java @@ -1,9 +1,8 @@ package com.braintreepayments.api; -import static com.braintreepayments.api.DropInClient.EXTRA_AUTHORIZATION; -import static com.braintreepayments.api.DropInClient.EXTRA_CHECKOUT_REQUEST; -import static com.braintreepayments.api.DropInClient.EXTRA_CHECKOUT_REQUEST_BUNDLE; -import static com.braintreepayments.api.DropInClient.EXTRA_SESSION_ID; +import static com.braintreepayments.api.DropInLauncher.EXTRA_AUTHORIZATION; +import static com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST; +import static com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST_BUNDLE; import static com.braintreepayments.api.DropInResult.EXTRA_ERROR; import android.content.Context; @@ -15,16 +14,15 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -class DropInActivityResultContract extends ActivityResultContract { +class DropInActivityResultContract extends ActivityResultContract { @NonNull @Override - public Intent createIntent(@NonNull Context context, DropInIntentData input) { + public Intent createIntent(@NonNull Context context, DropInLaunchInput input) { Bundle dropInRequestBundle = new Bundle(); dropInRequestBundle.putParcelable(EXTRA_CHECKOUT_REQUEST, input.getDropInRequest()); return new Intent(context, DropInActivity.class) .putExtra(EXTRA_CHECKOUT_REQUEST_BUNDLE, dropInRequestBundle) - .putExtra(EXTRA_SESSION_ID, input.getSessionId()) .putExtra(EXTRA_AUTHORIZATION, input.getAuthorization().toString()); } diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInClient.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInClient.java deleted file mode 100644 index 92fbe631c..000000000 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInClient.java +++ /dev/null @@ -1,370 +0,0 @@ -package com.braintreepayments.api; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.Lifecycle; - -/** - * Used to launch Drop-in and handle results - */ -public class DropInClient { - - static final String EXTRA_CHECKOUT_REQUEST = "com.braintreepayments.api.EXTRA_CHECKOUT_REQUEST"; - static final String EXTRA_CHECKOUT_REQUEST_BUNDLE = "com.braintreepayments.api.EXTRA_CHECKOUT_REQUEST_BUNDLE"; - static final String EXTRA_SESSION_ID = "com.braintreepayments.api.EXTRA_SESSION_ID"; - static final String EXTRA_AUTHORIZATION = "com.braintreepayments.api.EXTRA_AUTHORIZATION"; - static final String EXTRA_AUTHORIZATION_ERROR = "com.braintreepayments.api.EXTRA_AUTHORIZATION_ERROR"; - - @VisibleForTesting - final BraintreeClient braintreeClient; - - private final PaymentMethodClient paymentMethodClient; - private final GooglePayClient googlePayClient; - - private final DropInRequest dropInRequest; - - private final DropInSharedPreferences dropInSharedPreferences; - - private DropInListener listener; - - @VisibleForTesting - DropInLifecycleObserver observer; - - private static DropInClientParams createDefaultParams(Context context, String authorization, ClientTokenProvider clientTokenProvider, DropInRequest dropInRequest, FragmentActivity activity, Lifecycle lifecycle) { - - String customUrlScheme = null; - if (dropInRequest != null) { - customUrlScheme = dropInRequest.getCustomUrlScheme(); - } - - BraintreeOptions braintreeOptions = - new BraintreeOptions(context, null, customUrlScheme, authorization, clientTokenProvider, IntegrationType.DROP_IN); - - BraintreeClient braintreeClient = new BraintreeClient(braintreeOptions); - return new DropInClientParams() - .activity(activity) - .lifecycle(lifecycle) - .dropInRequest(dropInRequest) - .braintreeClient(braintreeClient) - .paymentMethodClient(new PaymentMethodClient(braintreeClient)) - .googlePayClient(new GooglePayClient(braintreeClient)) - .dropInSharedPreferences(DropInSharedPreferences.getInstance(context.getApplicationContext())); - } - - /** - * @param context a {@link Context} - * @param authorization a Tokenization Key or Client Token authorization String. - * @param dropInRequest a {@link DropInRequest} configured with options for launching Drop-in - * @deprecated use {@link #DropInClient(FragmentActivity, String)} or {@link #DropInClient(Fragment, String)} instead. - *

- * Create a new instance of {@link DropInClient}. - */ - @Deprecated - public DropInClient(Context context, String authorization, DropInRequest dropInRequest) { - this(createDefaultParams(context, authorization, null, dropInRequest, null, null)); - } - - /** - * @param activity a {@link FragmentActivity} - * @param dropInRequest a {@link DropInRequest} configured with options for launching Drop-in - * @param authorization a Tokenization Key authorization string - * @deprecated use {@link #DropInClient(FragmentActivity, String)} instead. - *

- * Create a new instance of {@link DropInClient} from within an Activity using a Tokenization Key authorization. - */ - @Deprecated - public DropInClient(FragmentActivity activity, DropInRequest dropInRequest, String authorization) { - this(activity, activity.getLifecycle(), authorization, dropInRequest); - } - - /** - * Create a new instance of {@link DropInClient} from within an Activity using a Tokenization Key authorization. - * - * @param activity a {@link FragmentActivity} - * @param authorization a Tokenization Key authorization string - */ - public DropInClient(FragmentActivity activity, String authorization) { - this(activity, activity.getLifecycle(), authorization, null); - } - - /** - * @param fragment a {@link Fragment} - * @param dropInRequest a {@link DropInRequest} configured with options for launching Drop-in - * @param authorization a Tokenization Key authorization string - * @deprecated use {@link #DropInClient(Fragment, String)} instead. - *

- * Create a new instance of {@link DropInClient} from within a Fragment using a Tokenization Key authorization. - */ - @Deprecated - public DropInClient(Fragment fragment, DropInRequest dropInRequest, String authorization) { - this(fragment.requireActivity(), fragment.getLifecycle(), authorization, dropInRequest); - } - - /** - * Create a new instance of {@link DropInClient} from within a Fragment using a Tokenization Key authorization. - * - * @param fragment a {@link Fragment} - * @param authorization a Tokenization Key authorization string - */ - public DropInClient(Fragment fragment, String authorization) { - this(fragment.requireActivity(), fragment.getLifecycle(), authorization, null); - } - - /** - * @param activity a {@link FragmentActivity} - * @param dropInRequest a {@link DropInRequest} configured with options for launching Drop-in - * @param clientTokenProvider a {@link ClientTokenProvider} - * @deprecated use {@link #DropInClient(FragmentActivity, ClientTokenProvider)} instead. - *

- * Create a new instance of {@link DropInClient} from within an Activity using a {@link ClientTokenProvider} to fetch authorization. - */ - @Deprecated - public DropInClient(FragmentActivity activity, DropInRequest dropInRequest, ClientTokenProvider clientTokenProvider) { - this(createDefaultParams(activity, null, clientTokenProvider, dropInRequest, activity, activity.getLifecycle())); - } - - /** - * Create a new instance of {@link DropInClient} from within an Activity using a {@link ClientTokenProvider} to fetch authorization. - * - * @param activity a {@link FragmentActivity} - * @param clientTokenProvider a {@link ClientTokenProvider} - */ - public DropInClient(FragmentActivity activity, ClientTokenProvider clientTokenProvider) { - this(createDefaultParams(activity, null, clientTokenProvider, null, activity, activity.getLifecycle())); - } - - /** - * @param fragment a {@link Fragment} - * @param dropInRequest a {@link DropInRequest} configured with options for launching Drop-in - * @param clientTokenProvider a {@link ClientTokenProvider} - * @deprecated use {@link #DropInClient(Fragment, ClientTokenProvider)} instead. - *

- * Create a new instance of {@link DropInClient} from within a Fragment using a {@link ClientTokenProvider} to fetch authorization. - */ - @Deprecated - public DropInClient(Fragment fragment, DropInRequest dropInRequest, ClientTokenProvider clientTokenProvider) { - this(createDefaultParams(fragment.requireActivity(), null, clientTokenProvider, dropInRequest, fragment.requireActivity(), fragment.getLifecycle())); - } - - /** - * Create a new instance of {@link DropInClient} from within a Fragment using a {@link ClientTokenProvider} to fetch authorization. - * - * @param fragment a {@link Fragment} - * @param clientTokenProvider a {@link ClientTokenProvider} - */ - public DropInClient(Fragment fragment, ClientTokenProvider clientTokenProvider) { - this(createDefaultParams(fragment.requireActivity(), null, clientTokenProvider, null, fragment.requireActivity(), fragment.getLifecycle())); - } - - DropInClient(FragmentActivity activity, Lifecycle lifecycle, String authorization, DropInRequest dropInRequest) { - this(createDefaultParams(activity, authorization, null, dropInRequest, activity, lifecycle)); - } - - @VisibleForTesting - DropInClient(DropInClientParams params) { - this.dropInRequest = params.getDropInRequest(); - this.braintreeClient = params.getBraintreeClient(); - this.googlePayClient = params.getGooglePayClient(); - this.paymentMethodClient = params.getPaymentMethodClient(); - this.dropInSharedPreferences = params.getDropInSharedPreferences(); - - FragmentActivity activity = params.getActivity(); - Lifecycle lifecycle = params.getLifecycle(); - if (activity != null && lifecycle != null) { - addObserver(activity, lifecycle); - } - } - - private void addObserver(@NonNull FragmentActivity activity, @NonNull Lifecycle lifecycle) { - observer = new DropInLifecycleObserver(activity.getActivityResultRegistry(), this); - lifecycle.addObserver(observer); - } - - /** - * Add a {@link DropInListener} to your client to receive results or errors from DropIn. - * Must be used with a {@link DropInClient} constructed with a {@link Fragment} or {@link FragmentActivity}. - * - * @param listener a {@link DropInListener} - */ - public void setListener(DropInListener listener) { - this.listener = listener; - } - - void getAuthorization(AuthorizationCallback callback) { - braintreeClient.getAuthorization(callback); - } - - /** - * @param activity the current {@link FragmentActivity} - * @param requestCode the request code for the activity that will be launched - * @deprecated use {@link #launchDropIn(DropInRequest)} instead - */ - @Deprecated - public void launchDropInForResult(FragmentActivity activity, int requestCode) { - getAuthorization((authorization, authorizationError) -> { - if (authorization != null) { - if (observer != null) { - DropInIntentData intentData = - new DropInIntentData(dropInRequest, authorization, braintreeClient.getSessionId()); - observer.launch(intentData); - } else { - Bundle dropInRequestBundle = new Bundle(); - dropInRequestBundle.putParcelable(EXTRA_CHECKOUT_REQUEST, dropInRequest); - Intent intent = new Intent(activity, DropInActivity.class) - .putExtra(EXTRA_CHECKOUT_REQUEST_BUNDLE, dropInRequestBundle) - .putExtra(EXTRA_SESSION_ID, braintreeClient.getSessionId()) - .putExtra(EXTRA_AUTHORIZATION, authorization.toString()); - activity.startActivityForResult(intent, requestCode); - } - } else if (authorizationError != null) { - if (listener != null) { - listener.onDropInFailure(authorizationError); - } else { - Intent intent = new Intent(activity, DropInActivity.class) - .putExtra(EXTRA_AUTHORIZATION_ERROR, authorizationError); - activity.startActivityForResult(intent, requestCode); - } - } - }); - } - - /** - * @see #DropInClient(Fragment, DropInRequest, String) - * @see #DropInClient(Fragment, DropInRequest, ClientTokenProvider) - * @see #DropInClient(FragmentActivity, DropInRequest, String) - * @see #DropInClient(FragmentActivity, DropInRequest, ClientTokenProvider) - * @deprecated use {@link #launchDropIn(DropInRequest)} instead. - * Called to launch a {@link DropInActivity}. - *

- * NOTE: This method requires {@link DropInClient} to be instantiated with either an Activity - * or with a Fragment. - */ - @Deprecated - public void launchDropIn() { - getAuthorization((authorization, authorizationError) -> { - if (authorization != null && observer != null) { - DropInIntentData intentData = - new DropInIntentData(dropInRequest, authorization, braintreeClient.getSessionId()); - observer.launch(intentData); - } else if (authorizationError != null && listener != null) { - listener.onDropInFailure(authorizationError); - } - }); - } - - /** - * Called to launch a {@link DropInActivity}. - *

- * NOTE: This method requires {@link DropInClient} to be instantiated with either an Activity - * or with a Fragment. - * - * @see #DropInClient(Fragment, String) - * @see #DropInClient(Fragment, ClientTokenProvider) - * @see #DropInClient(FragmentActivity, String) - * @see #DropInClient(FragmentActivity, ClientTokenProvider) - */ - public void launchDropIn(DropInRequest request) { - getAuthorization((authorization, authorizationError) -> { - if (authorization != null && observer != null) { - DropInIntentData intentData = - new DropInIntentData(request, authorization, braintreeClient.getSessionId()); - observer.launch(intentData); - } else if (authorizationError != null && listener != null) { - listener.onDropInFailure(authorizationError); - } - }); - } - - /** - * Called to get a user's existing payment method, if any. - * The payment method returned is not guaranteed to be the most recently added payment method. - * If your user already has an existing payment method, you may not need to show Drop-In. - *

- * Note: a client token must be used and will only return a payment method if it contains a - * customer id. - * - * @param activity the current {@link FragmentActivity} - * @param callback callback for handling result - */ - // NEXT_MAJOR_VERSION: - update this function name to more accurately represent the behavior of the function - public void fetchMostRecentPaymentMethod(FragmentActivity activity, final FetchMostRecentPaymentMethodCallback callback) { - getAuthorization(new AuthorizationCallback() { - @Override - public void onAuthorizationResult(@Nullable Authorization authorization, @Nullable Exception authError) { - if (authorization != null) { - - boolean isClientToken = (authorization instanceof ClientToken); - if (!isClientToken) { - InvalidArgumentException clientTokenRequiredError = - new InvalidArgumentException("DropInClient#fetchMostRecentPaymentMethods() must " + - "be called with a client token"); - callback.onResult(null, clientTokenRequiredError); - return; - } - - DropInPaymentMethod lastUsedPaymentMethod = - dropInSharedPreferences.getLastUsedPaymentMethod(); - - if (lastUsedPaymentMethod == DropInPaymentMethod.GOOGLE_PAY) { - googlePayClient.isReadyToPay(activity, (isReadyToPay, isReadyToPayError) -> { - if (isReadyToPay) { - DropInResult result = new DropInResult(); - result.setPaymentMethodType(DropInPaymentMethod.GOOGLE_PAY); - callback.onResult(result, null); - } else { - getPaymentMethodNonces(callback); - } - }); - } else { - getPaymentMethodNonces(callback); - } - } else { - callback.onResult(null, authError); - } - } - }); - } - - private void getPaymentMethodNonces(final FetchMostRecentPaymentMethodCallback callback) { - paymentMethodClient.getPaymentMethodNonces((paymentMethodNonceList, error) -> { - if (paymentMethodNonceList != null) { - DropInResult result = new DropInResult(); - if (paymentMethodNonceList.size() > 0) { - PaymentMethodNonce paymentMethod = paymentMethodNonceList.get(0); - result.setPaymentMethodNonce(paymentMethod); - } - callback.onResult(result, null); - } else if (error != null) { - callback.onResult(null, error); - } - }); - } - - void onDropInResult(DropInResult dropInResult) { - if (dropInResult != null && listener != null) { - Exception error = dropInResult.getError(); - if (error != null) { - listener.onDropInFailure(error); - } else { - listener.onDropInSuccess(dropInResult); - } - } - } - - /** - * For clients using a {@link ClientTokenProvider}, call this method to invalidate the existing, - * cached client token. A new client token will be fetched by the SDK when it is needed. - *

- * For clients not using a {@link ClientTokenProvider}, this method does nothing. - */ - public void invalidateClientToken() { - braintreeClient.invalidateClientToken(); - } -} diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInClientParams.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInClientParams.java deleted file mode 100644 index d76cd0ae7..000000000 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInClientParams.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.braintreepayments.api; - -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.Lifecycle; - -class DropInClientParams { - - private DropInRequest dropInRequest; - - private BraintreeClient braintreeClient; - private GooglePayClient googlePayClient; - private PaymentMethodClient paymentMethodClient; - private DropInSharedPreferences dropInSharedPreferences; - private FragmentActivity activity; - private Lifecycle lifecycle; - - DropInRequest getDropInRequest() { - return dropInRequest; - } - - DropInClientParams dropInRequest(DropInRequest dropInRequest) { - this.dropInRequest = dropInRequest; - return this; - } - - BraintreeClient getBraintreeClient() { - return braintreeClient; - } - - DropInClientParams braintreeClient(BraintreeClient braintreeClient) { - this.braintreeClient = braintreeClient; - return this; - } - - GooglePayClient getGooglePayClient() { - return googlePayClient; - } - - DropInClientParams googlePayClient(GooglePayClient googlePayClient) { - this.googlePayClient = googlePayClient; - return this; - } - - PaymentMethodClient getPaymentMethodClient() { - return paymentMethodClient; - } - - DropInClientParams paymentMethodClient(PaymentMethodClient paymentMethodClient) { - this.paymentMethodClient = paymentMethodClient; - return this; - } - - DropInClientParams dropInSharedPreferences(DropInSharedPreferences dropInSharedPreferences) { - this.dropInSharedPreferences = dropInSharedPreferences; - return this; - } - - DropInSharedPreferences getDropInSharedPreferences() { - return dropInSharedPreferences; - } - - DropInClientParams activity(FragmentActivity activity) { - this.activity = activity; - return this; - } - - FragmentActivity getActivity() { - return activity; - } - - DropInClientParams lifecycle(Lifecycle lifecycle) { - this.lifecycle = lifecycle; - return this; - } - - Lifecycle getLifecycle() { - return lifecycle; - } -} diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java index 0560ea8c9..e63ea12aa 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInInternalClient.java @@ -37,11 +37,11 @@ class DropInInternalClient { private final PaymentMethodInspector paymentMethodInspector = new PaymentMethodInspector(); - private static DropInInternalClientParams createDefaultParams(Context context, String authorization, DropInRequest dropInRequest, String sessionId) { + private static DropInInternalClientParams createDefaultParams(Context context, String authorization, DropInRequest dropInRequest) { String customUrlScheme = dropInRequest.getCustomUrlScheme(); BraintreeOptions braintreeOptions = - new BraintreeOptions(context, sessionId, customUrlScheme, authorization, null, IntegrationType.DROP_IN); + new BraintreeOptions(context, null, customUrlScheme, authorization, null, IntegrationType.DROP_IN); BraintreeClient braintreeClient = new BraintreeClient(braintreeOptions); @@ -59,8 +59,8 @@ private static DropInInternalClientParams createDefaultParams(Context context, S .dropInSharedPreferences(DropInSharedPreferences.getInstance(context.getApplicationContext())); } - DropInInternalClient(FragmentActivity activity, String authorization, String sessionId, DropInRequest dropInRequest) { - this(createDefaultParams(activity, authorization, dropInRequest, sessionId)); + DropInInternalClient(FragmentActivity activity, String authorization, DropInRequest dropInRequest) { + this(createDefaultParams(activity, authorization, dropInRequest)); } @VisibleForTesting diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInIntentData.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInLaunchInput.java similarity index 59% rename from Drop-In/src/main/java/com/braintreepayments/api/DropInIntentData.java rename to Drop-In/src/main/java/com/braintreepayments/api/DropInLaunchInput.java index 79edccdfa..4d0fc5fee 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInIntentData.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInLaunchInput.java @@ -1,13 +1,11 @@ package com.braintreepayments.api; -class DropInIntentData { +class DropInLaunchInput { private final Authorization authorization; private final DropInRequest dropInRequest; - private final String sessionId; - DropInIntentData(DropInRequest dropInRequest, Authorization authorization, String sessionId) { - this.sessionId = sessionId; + DropInLaunchInput(DropInRequest dropInRequest, Authorization authorization) { this.dropInRequest = dropInRequest; this.authorization = authorization; } @@ -19,8 +17,4 @@ DropInRequest getDropInRequest() { Authorization getAuthorization() { return authorization; } - - String getSessionId() { - return sessionId; - } } diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInLauncher.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInLauncher.java new file mode 100644 index 000000000..b22de8365 --- /dev/null +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInLauncher.java @@ -0,0 +1,34 @@ +package com.braintreepayments.api; + +import androidx.activity.ComponentActivity; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.ActivityResultRegistry; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.DefaultLifecycleObserver; + +public class DropInLauncher implements DefaultLifecycleObserver { + + private static final String DROP_IN_RESULT = "com.braintreepayments.api.DropIn.RESULT"; + + static final String EXTRA_CHECKOUT_REQUEST = "com.braintreepayments.api.EXTRA_CHECKOUT_REQUEST"; + static final String EXTRA_CHECKOUT_REQUEST_BUNDLE = "com.braintreepayments.api.EXTRA_CHECKOUT_REQUEST_BUNDLE"; + static final String EXTRA_AUTHORIZATION = "com.braintreepayments.api.EXTRA_AUTHORIZATION"; + static final String EXTRA_AUTHORIZATION_ERROR = "com.braintreepayments.api.EXTRA_AUTHORIZATION_ERROR"; + + + @VisibleForTesting + private ActivityResultLauncher activityLauncher; + + public DropInLauncher(ComponentActivity activity, DropInLauncherCallback callback) { + ActivityResultRegistry registry = activity.getActivityResultRegistry(); + activityLauncher = registry.register( + DROP_IN_RESULT, activity, new DropInActivityResultContract(), callback); + } + + public void start(String authString, DropInRequest dropInRequest) { + Authorization authorization = Authorization.fromString(authString); + DropInLaunchInput launchIntent = + new DropInLaunchInput(dropInRequest, authorization); + activityLauncher.launch(launchIntent); + } +} diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInLauncherCallback.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInLauncherCallback.java new file mode 100644 index 000000000..8b8c0bb12 --- /dev/null +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInLauncherCallback.java @@ -0,0 +1,17 @@ +package com.braintreepayments.api; + +import androidx.activity.result.ActivityResultCallback; + +/** + * Callback used to receive an Activity result from {@link DropInLauncher}. + */ +public interface DropInLauncherCallback extends ActivityResultCallback { + + /** + * Provides the result of launching {@link DropInActivity}. + * + * @param result DropInResult + */ + @Override + void onActivityResult(DropInResult result); +} \ No newline at end of file diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInLifecycleObserver.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInLifecycleObserver.java deleted file mode 100644 index 99d753525..000000000 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInLifecycleObserver.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.braintreepayments.api; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.ActivityResultRegistry; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.LifecycleOwner; - -class DropInLifecycleObserver implements DefaultLifecycleObserver { - - private static final String DROP_IN_RESULT = "com.braintreepayments.api.DropIn.RESULT"; - - @VisibleForTesting - DropInClient dropInClient; - - @VisibleForTesting - final ActivityResultRegistry activityResultRegistry; - - @VisibleForTesting - ActivityResultLauncher activityLauncher; - - DropInLifecycleObserver(ActivityResultRegistry activityResultRegistry, DropInClient dropInClient) { - this.dropInClient = dropInClient; - this.activityResultRegistry = activityResultRegistry; - } - - @Override - public void onCreate(@NonNull LifecycleOwner owner) { - activityLauncher = activityResultRegistry.register( - DROP_IN_RESULT, - owner, - new DropInActivityResultContract(), - dropInResult -> dropInClient.onDropInResult(dropInResult)); - } - - void launch(DropInIntentData intentData) { - activityLauncher.launch(intentData); - } -} diff --git a/Drop-In/src/main/java/com/braintreepayments/api/DropInResult.java b/Drop-In/src/main/java/com/braintreepayments/api/DropInResult.java index 78da796d6..30cca4c3a 100644 --- a/Drop-In/src/main/java/com/braintreepayments/api/DropInResult.java +++ b/Drop-In/src/main/java/com/braintreepayments/api/DropInResult.java @@ -90,7 +90,8 @@ public String getDeviceData() { return deviceData; } - Exception getError() { + @Nullable + public Exception getError() { return error; } diff --git a/Drop-In/src/main/java/com/braintreepayments/api/RecentPaymentMethodsClient.java b/Drop-In/src/main/java/com/braintreepayments/api/RecentPaymentMethodsClient.java new file mode 100644 index 000000000..73ef5517d --- /dev/null +++ b/Drop-In/src/main/java/com/braintreepayments/api/RecentPaymentMethodsClient.java @@ -0,0 +1,117 @@ +package com.braintreepayments.api; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentActivity; + +/** + * Fetches recently used payment method nonces for the user associated with a given client token. + */ +public class RecentPaymentMethodsClient { + + private final BraintreeClient braintreeClient; + + private final GooglePayClient googlePayClient; + + private final PaymentMethodClient paymentMethodClient; + private final DropInSharedPreferences sharedPreferences; + + /** + * Create a new instance of {@link RecentPaymentMethodsClient}. + * + * @param context Android context + * @param clientToken Client Token + */ + public RecentPaymentMethodsClient(@NonNull Context context, @NonNull String clientToken) { + this(context, new BraintreeClient(context, clientToken)); + } + + private RecentPaymentMethodsClient( + @NonNull Context context, + @NonNull BraintreeClient braintreeClient + ) { + this( + braintreeClient, + new GooglePayClient(braintreeClient), + new PaymentMethodClient(braintreeClient), + DropInSharedPreferences.getInstance(context) + ); + } + + @VisibleForTesting + RecentPaymentMethodsClient( + @NonNull BraintreeClient braintreeClient, + @NonNull GooglePayClient googlePayClient, + @NonNull PaymentMethodClient paymentMethodClient, + @NonNull DropInSharedPreferences sharedPreferences + ) { + this.braintreeClient = braintreeClient; + this.googlePayClient = googlePayClient; + this.paymentMethodClient = paymentMethodClient; + this.sharedPreferences = sharedPreferences; + } + + /** + * Called to get a user's existing payment method, if any. + * The payment method returned is not guaranteed to be the most recently added payment method. + * If your user already has an existing payment method, you may not need to show Drop-In. + *

+ * Note: a client token must be used and will only return a payment method if it contains a + * customer id. + * + * @param activity the current {@link FragmentActivity} + * @param callback callback for handling result + */ + // NEXT_MAJOR_VERSION: - update this function name to more accurately represent the behavior of the function + public void fetchMostRecentPaymentMethod(FragmentActivity activity, final FetchMostRecentPaymentMethodCallback callback) { + braintreeClient.getAuthorization((authorization, authError) -> { + if (authorization != null) { + + boolean isClientToken = (authorization instanceof ClientToken); + if (!isClientToken) { + InvalidArgumentException clientTokenRequiredError = + new InvalidArgumentException("DropInClient#fetchMostRecentPaymentMethods() must " + + "be called with a client token"); + callback.onResult(null, clientTokenRequiredError); + return; + } + + DropInPaymentMethod lastUsedPaymentMethod = + sharedPreferences.getLastUsedPaymentMethod(); + + if (lastUsedPaymentMethod == DropInPaymentMethod.GOOGLE_PAY) { + googlePayClient.isReadyToPay(activity, (isReadyToPay, isReadyToPayError) -> { + if (isReadyToPay) { + DropInResult result = new DropInResult(); + result.setPaymentMethodType(DropInPaymentMethod.GOOGLE_PAY); + callback.onResult(result, null); + } else { + getPaymentMethodNonces(callback); + } + }); + } else { + getPaymentMethodNonces(callback); + } + } else { + callback.onResult(null, authError); + } + }); + } + + private void getPaymentMethodNonces(final FetchMostRecentPaymentMethodCallback callback) { + paymentMethodClient.getPaymentMethodNonces((paymentMethodNonceList, error) -> { + if (paymentMethodNonceList != null) { + DropInResult result = new DropInResult(); + if (paymentMethodNonceList.size() > 0) { + PaymentMethodNonce paymentMethod = paymentMethodNonceList.get(0); + result.setPaymentMethodNonce(paymentMethod); + } + callback.onResult(result, null); + } else if (error != null) { + callback.onResult(null, error); + } + }); + } +} diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityResultContractUnitTest.java b/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityResultContractUnitTest.java index 16ae70412..e47b50cf6 100644 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityResultContractUnitTest.java +++ b/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityResultContractUnitTest.java @@ -3,10 +3,9 @@ import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_FIRST_USER; import static android.app.Activity.RESULT_OK; -import static com.braintreepayments.api.DropInClient.EXTRA_AUTHORIZATION; -import static com.braintreepayments.api.DropInClient.EXTRA_CHECKOUT_REQUEST; -import static com.braintreepayments.api.DropInClient.EXTRA_CHECKOUT_REQUEST_BUNDLE; -import static com.braintreepayments.api.DropInClient.EXTRA_SESSION_ID; +import static com.braintreepayments.api.DropInLauncher.EXTRA_AUTHORIZATION; +import static com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST; +import static com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST_BUNDLE; import static com.braintreepayments.api.DropInResult.EXTRA_DROP_IN_RESULT; import static com.braintreepayments.api.DropInResult.EXTRA_ERROR; import static org.junit.Assert.assertEquals; @@ -42,14 +41,13 @@ public void createIntent_returnsIntentWithExtras() { DropInRequest dropInRequest = new DropInRequest(); Authorization authorization = Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN); - DropInIntentData input = new DropInIntentData(dropInRequest, authorization, sessionId); + DropInLaunchInput input = new DropInLaunchInput(dropInRequest, authorization); Intent intent = sut.createIntent(context, input); String expectedClass = "com.braintreepayments.api.DropInActivity"; assertEquals(expectedClass, intent.getComponent().getClassName()); - assertEquals("sample-session-id", intent.getStringExtra(EXTRA_SESSION_ID)); assertEquals(Fixtures.BASE64_CLIENT_TOKEN, intent.getStringExtra(EXTRA_AUTHORIZATION)); Bundle requestBundle = intent.getBundleExtra(EXTRA_CHECKOUT_REQUEST_BUNDLE); diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt b/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt index 9d22afb14..18b811c18 100644 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt +++ b/Drop-In/src/test/java/com/braintreepayments/api/DropInActivityUnitTest.kt @@ -7,7 +7,9 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.test.platform.app.InstrumentationRegistry -import com.braintreepayments.api.DropInClient.EXTRA_CHECKOUT_REQUEST +import com.braintreepayments.api.DropInLauncher.EXTRA_AUTHORIZATION_ERROR +import com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST +import com.braintreepayments.api.DropInLauncher.EXTRA_CHECKOUT_REQUEST_BUNDLE import com.braintreepayments.cardform.utils.CardType import org.json.JSONObject import org.junit.After @@ -1125,7 +1127,7 @@ class DropInActivityUnitTest { private fun setupDropInActivityWithError(authError: Exception) { val context = InstrumentationRegistry.getInstrumentation().targetContext val intent = Intent(context, DropInActivity::class.java) - .putExtra(DropInClient.EXTRA_AUTHORIZATION_ERROR, authError) + .putExtra(EXTRA_AUTHORIZATION_ERROR, authError) activityController = buildActivity(DropInActivity::class.java, intent) activity = activityController.get() @@ -1137,7 +1139,7 @@ class DropInActivityUnitTest { val dropInRequestBundle = Bundle() dropInRequestBundle.putParcelable(EXTRA_CHECKOUT_REQUEST, dropInRequest) val intent = Intent(context, DropInActivity::class.java) - intent.putExtra(DropInClient.EXTRA_CHECKOUT_REQUEST_BUNDLE, dropInRequestBundle) + intent.putExtra(EXTRA_CHECKOUT_REQUEST_BUNDLE, dropInRequestBundle) activityController = buildActivity(DropInActivity::class.java, intent) activity = activityController.get() diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInClientUnitTest.java b/Drop-In/src/test/java/com/braintreepayments/api/DropInClientUnitTest.java deleted file mode 100644 index 7aeb7c834..000000000 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInClientUnitTest.java +++ /dev/null @@ -1,438 +0,0 @@ -package com.braintreepayments.api; - -import static junit.framework.TestCase.assertNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.fragment.app.FragmentActivity; -import androidx.test.core.app.ApplicationProvider; -import androidx.work.testing.WorkManagerTestInitHelper; - -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ActivityController; - -import java.util.ArrayList; - -@RunWith(RobolectricTestRunner.class) -public class DropInClientUnitTest { - - private FragmentActivity activity; - private DropInSharedPreferences dropInSharedPreferences; - - @Before - public void beforeEach() { - - dropInSharedPreferences = mock(DropInSharedPreferences.class); - ActivityController activityController = - Robolectric.buildActivity(FragmentActivity.class); - activity = activityController.get(); - // This suppresses errors from WorkManager initialization within BraintreeClient initialization (AnalyticsClient) - WorkManagerTestInitHelper.initializeTestWorkManager(ApplicationProvider.getApplicationContext()); - } - - @Test - public void constructor_setsIntegrationTypeDropIn() { - DropInClient sut = new DropInClient(ApplicationProvider.getApplicationContext(), Fixtures.TOKENIZATION_KEY, new DropInRequest()); - assertEquals(IntegrationType.DROP_IN, sut.braintreeClient.getIntegrationType()); - } - - @Test - public void publicConstructor_setsBraintreeClientWithSessionId() { - DropInClient sut = new DropInClient(ApplicationProvider.getApplicationContext(), Fixtures.TOKENIZATION_KEY, new DropInRequest()); - assertNotNull(sut.braintreeClient.getSessionId()); - } - - @Test - public void getAuthorization_forwardsInvocationToBraintreeClient() { - Authorization authorization = mock(Authorization.class); - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationSuccess(authorization) - .build(); - - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient); - - DropInClient sut = new DropInClient(params); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.getAuthorization(callback); - verify(braintreeClient).getAuthorization(callback); - } - - @Test - public void fetchMostRecentPaymentMethod_forwardsAuthorizationFetchErrors() { - Exception authError = new Exception("auth error"); - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationError(authError) - .build(); - - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient) - .dropInRequest(new DropInRequest()); - - DropInClient sut = new DropInClient(params); - - FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); - sut.fetchMostRecentPaymentMethod(activity, callback); - - verify(callback).onResult(null, authError); - } - - @Test - public void fetchMostRecentPaymentMethod_callsBackWithErrorIfInvalidClientTokenWasUsed() { - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationSuccess(Authorization.fromString(Fixtures.TOKENIZATION_KEY)) - .build(); - - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient) - .dropInRequest(new DropInRequest()); - - DropInClient sut = new DropInClient(params); - - FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); - sut.fetchMostRecentPaymentMethod(activity, callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(InvalidArgumentException.class); - verify(callback).onResult((DropInResult) isNull(), captor.capture()); - - InvalidArgumentException exception = captor.getValue(); - assertEquals("DropInClient#fetchMostRecentPaymentMethods() must be called with a client token", exception.getMessage()); - } - - @Test - public void fetchMostRecentPaymentMethod_callsBackWithResultIfLastUsedPaymentMethodTypeWasPayWithGoogle() throws JSONException { - - GooglePayClient googlePayClient = new MockGooglePayClientBuilder() - .isReadyToPaySuccess(true) - .build(); - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) - .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) - .build(); - - DropInClientParams params = new DropInClientParams() - .dropInRequest(new DropInRequest()) - .braintreeClient(braintreeClient) - .dropInSharedPreferences(dropInSharedPreferences) - .googlePayClient(googlePayClient); - - when( - dropInSharedPreferences.getLastUsedPaymentMethod() - ).thenReturn(DropInPaymentMethod.GOOGLE_PAY); - - DropInClient sut = new DropInClient(params); - - FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); - sut.fetchMostRecentPaymentMethod(activity, callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); - verify(callback).onResult(captor.capture(), (Exception) isNull()); - - DropInResult result = captor.getValue(); - assertEquals(DropInPaymentMethod.GOOGLE_PAY, result.getPaymentMethodType()); - assertNull(result.getPaymentMethodNonce()); - } - - @Test - public void fetchMostRecentPaymentMethod_doesNotCallBackWithPayWithGoogleIfPayWithGoogleIsNotAvailable() throws JSONException { - - GooglePayClient googlePayClient = new MockGooglePayClientBuilder() - .isReadyToPaySuccess(false) - .build(); - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) - .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) - .build(); - - ArrayList paymentMethods = new ArrayList<>(); - paymentMethods.add(CardNonce.fromJSON(new JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE))); - paymentMethods.add(GooglePayCardNonce.fromJSON(new JSONObject(Fixtures.GOOGLE_PAY_NETWORK_TOKENIZED_RESPONSE))); - PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() - .getPaymentMethodNoncesSuccess(paymentMethods) - .build(); - - DropInClientParams params = new DropInClientParams() - .dropInRequest(new DropInRequest()) - .braintreeClient(braintreeClient) - .paymentMethodClient(paymentMethodClient) - .googlePayClient(googlePayClient) - .dropInSharedPreferences(dropInSharedPreferences); - - when( - dropInSharedPreferences.getLastUsedPaymentMethod() - ).thenReturn(DropInPaymentMethod.GOOGLE_PAY); - - DropInClient sut = new DropInClient(params); - - FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); - sut.fetchMostRecentPaymentMethod(activity, callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); - verify(callback).onResult(captor.capture(), (Exception) isNull()); - - DropInResult result = captor.getValue(); - assertEquals(DropInPaymentMethod.VISA, result.getPaymentMethodType()); - assertNotNull(result.getPaymentMethodNonce()); - assertEquals("11", ((CardNonce) result.getPaymentMethodNonce()).getLastTwo()); - } - - @Test - public void fetchMostRecentPaymentMethod_callsBackWithErrorOnGetPaymentMethodsError() { - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) - .build(); - - PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() - .getPaymentMethodNoncesError(new BraintreeException("Error occurred")) - .build(); - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient) - .paymentMethodClient(paymentMethodClient) - .dropInSharedPreferences(dropInSharedPreferences) - .dropInRequest(new DropInRequest()); - - DropInClient sut = new DropInClient(params); - - FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); - sut.fetchMostRecentPaymentMethod(activity, callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(BraintreeException.class); - verify(callback).onResult((DropInResult) isNull(), captor.capture()); - - BraintreeException exception = captor.getValue(); - assertEquals("Error occurred", exception.getMessage()); - } - - @Test - public void fetchMostRecentPaymentMethod_callsBackWithResultWhenThereIsAPaymentMethod() - throws JSONException { - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) - .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) - .build(); - - ArrayList paymentMethods = new ArrayList<>(); - paymentMethods.add(CardNonce.fromJSON(new JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE))); - - PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() - .getPaymentMethodNoncesSuccess(paymentMethods) - .build(); - - DropInClientParams params = new DropInClientParams() - .dropInRequest(new DropInRequest()) - .braintreeClient(braintreeClient) - .dropInSharedPreferences(dropInSharedPreferences) - .paymentMethodClient(paymentMethodClient); - - DropInClient sut = new DropInClient(params); - - FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); - sut.fetchMostRecentPaymentMethod(activity, callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); - verify(callback).onResult(captor.capture(), (Exception) isNull()); - - DropInResult result = captor.getValue(); - assertEquals(DropInPaymentMethod.VISA, result.getPaymentMethodType()); - assertNotNull(result.getPaymentMethodNonce()); - assertEquals("11", ((CardNonce) result.getPaymentMethodNonce()).getLastTwo()); - } - - @Test - public void fetchMostRecentPaymentMethod_callsBackWithNullResultWhenThereAreNoPaymentMethods() throws JSONException { - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) - .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) - .build(); - - ArrayList paymentMethods = new ArrayList<>(); - - PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() - .getPaymentMethodNoncesSuccess(paymentMethods) - .build(); - - DropInClientParams params = new DropInClientParams() - .dropInRequest(new DropInRequest()) - .braintreeClient(braintreeClient) - .dropInSharedPreferences(dropInSharedPreferences) - .paymentMethodClient(paymentMethodClient); - - DropInClient sut = new DropInClient(params); - - FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); - sut.fetchMostRecentPaymentMethod(activity, callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); - verify(callback).onResult(captor.capture(), (Exception) isNull()); - - DropInResult result = captor.getValue(); - assertNull(result.getPaymentMethodType()); - assertNull(result.getPaymentMethodNonce()); - } - - @Test - public void legacy_launchDropInForResult_withListener_forwardsAuthorizationFetchErrors() { - Exception authError = new Exception("auth error"); - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .sessionId("session-id") - .authorizationError(authError) - .build(); - - DropInRequest dropInRequest = new DropInRequest(); - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient) - .dropInRequest(dropInRequest); - - DropInClient sut = new DropInClient(params); - - DropInListener listener = mock(DropInListener.class); - sut.setListener(listener); - - FragmentActivity activity = mock(FragmentActivity.class); - sut.launchDropInForResult(activity, 123); - - verify(listener).onDropInFailure(authError); - } - - @Test - public void legacy_launchDropInForResult_withoutListener_launchesDropInActivityWithError() { - Exception authError = new Exception("auth error"); - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .sessionId("session-id") - .authorizationError(authError) - .build(); - - DropInRequest dropInRequest = new DropInRequest(); - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient) - .dropInRequest(dropInRequest); - - DropInClient sut = new DropInClient(params); - - FragmentActivity activity = mock(FragmentActivity.class); - sut.launchDropInForResult(activity, 123); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); - verify(activity).startActivityForResult(captor.capture(), eq(123)); - - Intent intent = captor.getValue(); - Exception error = (Exception) intent.getSerializableExtra(DropInClient.EXTRA_AUTHORIZATION_ERROR); - assertEquals("auth error", error.getMessage()); - } - - @Test - public void legacy_launchDropInForResult_launchesDropInActivityWithIntentExtras() { - Authorization authorization = mock(Authorization.class); - when(authorization.toString()).thenReturn("authorization"); - - BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .sessionId("session-id") - .authorizationSuccess(authorization) - .build(); - - DropInRequest dropInRequest = new DropInRequest(); - dropInRequest.setVaultManagerEnabled(true); - - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient) - .dropInRequest(dropInRequest); - - DropInClient sut = new DropInClient(params); - - FragmentActivity activity = mock(FragmentActivity.class); - sut.launchDropInForResult(activity, 123); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); - verify(activity).startActivityForResult(captor.capture(), eq(123)); - - Intent intent = captor.getValue(); - assertEquals("session-id", intent.getStringExtra(DropInClient.EXTRA_SESSION_ID)); - assertEquals("authorization", intent.getStringExtra(DropInClient.EXTRA_AUTHORIZATION)); - - Bundle bundle = intent.getParcelableExtra(DropInClient.EXTRA_CHECKOUT_REQUEST_BUNDLE); - bundle.setClassLoader(DropInRequest.class.getClassLoader()); - DropInRequest dropInRequestExtra = bundle.getParcelable(DropInClient.EXTRA_CHECKOUT_REQUEST); - assertTrue(dropInRequestExtra.isVaultManagerEnabled()); - } - - @Test - public void onDropInResult_whenResultHasNoError_notifiesListenerOfSuccessViaCallback() { - DropInClientParams params = new DropInClientParams(); - DropInClient sut = new DropInClient(params); - - DropInListener listener = mock(DropInListener.class); - sut.setListener(listener); - - DropInResult dropInResult = new DropInResult(); - sut.onDropInResult(dropInResult); - - verify(listener).onDropInSuccess(dropInResult); - } - - @Test - public void onDropInResult_whenResultHasError_notifiesListenerOfErrorViaCallback() { - DropInClientParams params = new DropInClientParams(); - DropInClient sut = new DropInClient(params); - - DropInListener listener = mock(DropInListener.class); - sut.setListener(listener); - - DropInResult dropInResult = new DropInResult(); - Exception error = new Exception("sample error"); - dropInResult.setError(error); - - sut.onDropInResult(dropInResult); - verify(listener).onDropInFailure(error); - } - - @Test - public void onDropInResult_whenResultIsNull_doesNothing() { - DropInClientParams params = new DropInClientParams(); - DropInClient sut = new DropInClient(params); - - DropInListener listener = mock(DropInListener.class); - sut.setListener(listener); - - sut.onDropInResult(null); - verifyNoInteractions(listener); - } - - @Test - public void onDropInResult_whenListenerIsNull_doesNothing() { - DropInClientParams params = new DropInClientParams(); - DropInClient sut = new DropInClient(params); - - - DropInResult dropInResult = new DropInResult(); - sut.onDropInResult(dropInResult); - } - - @Test - public void invalidateClientToken_forwardsInvocationIntoBraintreeClient() { - BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - DropInClientParams params = new DropInClientParams() - .braintreeClient(braintreeClient); - DropInClient sut = new DropInClient(params); - - sut.invalidateClientToken(); - verify(braintreeClient).invalidateClientToken(); - } -} \ No newline at end of file diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInClientUnitTestKt.kt b/Drop-In/src/test/java/com/braintreepayments/api/DropInClientUnitTestKt.kt deleted file mode 100644 index f55e98549..000000000 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInClientUnitTestKt.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.braintreepayments.api - -import android.content.Context -import androidx.activity.result.ActivityResultRegistry -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ApplicationProvider -import androidx.work.testing.WorkManagerTestInitHelper -import io.mockk.* -import org.json.JSONObject -import org.junit.Assert.* -import org.junit.Before -import org.junit.BeforeClass -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -// TODO: Slowly migrate the entire DropInClientUnitTest to Kotlin -@RunWith(RobolectricTestRunner::class) -class DropInClientUnitTestKt { - - companion object { - - @BeforeClass - @JvmStatic - fun beforeAll() { - // required for mockk since AuthorizationCallback is package-private - registerInstanceFactory { mockk() } - } - } - - private val applicationContext: Context = ApplicationProvider.getApplicationContext() - - private lateinit var fragment: Fragment - private lateinit var fragmentLifecycle: Lifecycle - - private lateinit var activity: FragmentActivity - private lateinit var activityLifecycle: Lifecycle - - private lateinit var resultRegistry: ActivityResultRegistry - - private lateinit var dropInRequest: DropInRequest - private lateinit var braintreeClient: BraintreeClient - - private lateinit var clientToken: Authorization - private lateinit var clientTokenProvider: ClientTokenProvider - - @Before - fun beforeEach() { - - dropInRequest = DropInRequest() - braintreeClient = mockk() - - clientToken = Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN) - clientTokenProvider = mockk() - - activity = mockk(relaxed = true) - every { activity.applicationContext } returns applicationContext - - resultRegistry = mockk() - every { activity.activityResultRegistry } returns resultRegistry - - activityLifecycle = mockk(relaxed = true) - every { activity.lifecycle } returns activityLifecycle - - fragment = mockk() - every { fragment.requireActivity() } returns activity - - fragmentLifecycle = mockk() - every { fragment.lifecycle } returns fragmentLifecycle - - // This suppresses errors from WorkManager initialization within BraintreeClient - // initialization (AnalyticsClient) - WorkManagerTestInitHelper.initializeTestWorkManager(applicationContext) - } - - @Test - fun constructor_setsIntegrationTypeDropIn() { - val sut = DropInClient(activity, dropInRequest, clientTokenProvider) - assertEquals(IntegrationType.DROP_IN, sut.braintreeClient.integrationType) - } - - @Test - fun constructor_withFragment_registersLifecycleObserver() { - val observerSlot = slot() - justRun { fragmentLifecycle.addObserver(capture(observerSlot)) } - - val sut = DropInClient(fragment, dropInRequest, clientTokenProvider) - val capturedObserver = observerSlot.captured - - assertSame(capturedObserver, sut.observer) - assertSame(resultRegistry, capturedObserver.activityResultRegistry) - assertSame(sut, capturedObserver.dropInClient) - } - - @Test - fun constructor_withActivity_registersLifecycleObserver() { - val observerSlot = slot() - justRun { activityLifecycle.addObserver(capture(observerSlot)) } - - val sut = DropInClient(activity, dropInRequest, clientTokenProvider) - val capturedObserver = observerSlot.captured - - assertSame(capturedObserver, sut.observer) - assertSame(resultRegistry, capturedObserver.activityResultRegistry) - assertSame(sut, capturedObserver.dropInClient) - } - - @Test - fun constructor_withContext_doesNotRegisterLifecycleObserver() { - val sut = DropInClient(applicationContext, Fixtures.TOKENIZATION_KEY, dropInRequest) - assertNull(sut.observer) - } - - @Test - fun legacy_launchDropInForResult_withObserver_launchesWithObserver() { - every { braintreeClient.sessionId } returns "sample-session-id" - - every { braintreeClient.getAuthorization(any()) } answers { call -> - val callback = call.invocation.args[0] as AuthorizationCallback - callback.onAuthorizationResult(clientToken, null) - } - - val params = DropInClientParams() - .dropInRequest(dropInRequest) - .braintreeClient(braintreeClient) - val sut = DropInClient(params) - sut.observer = mockk() - - val intentDataSlot = slot() - justRun { sut.observer.launch(capture(intentDataSlot)) } - - sut.launchDropInForResult(activity, 123) - val capturedIntentData = intentDataSlot.captured - - assertEquals("sample-session-id", capturedIntentData.sessionId) - assertEquals(clientToken.toString(), capturedIntentData.authorization.toString()) - assertNotNull(capturedIntentData.dropInRequest) - } - - @Test - fun launchDropIn_withObserver_launchesWithObserver() { - every { braintreeClient.sessionId } returns "sample-session-id" - - every { braintreeClient.getAuthorization(any()) } answers { call -> - val callback = call.invocation.args[0] as AuthorizationCallback - callback.onAuthorizationResult(clientToken, null) - } - - val params = DropInClientParams() - .dropInRequest(dropInRequest) - .braintreeClient(braintreeClient) - val sut = DropInClient(params) - sut.observer = mockk() - - val intentDataSlot = slot() - justRun { sut.observer.launch(capture(intentDataSlot)) } - - sut.launchDropIn() - val capturedIntentData = intentDataSlot.captured - - assertEquals("sample-session-id", capturedIntentData.sessionId) - assertEquals(clientToken.toString(), capturedIntentData.authorization.toString()) - assertNotNull(capturedIntentData.dropInRequest) - } - - @Test - fun launchDropIn_forwardsAuthorizationFetchErrorsToListener() { - every { braintreeClient.sessionId } returns "sample-session-id" - - val authError = Exception("auth error") - every { braintreeClient.getAuthorization(any()) } answers { call -> - val callback = call.invocation.args[0] as AuthorizationCallback - callback.onAuthorizationResult(null, authError) - } - - val params = DropInClientParams() - .dropInRequest(dropInRequest) - .braintreeClient(braintreeClient) - val sut = DropInClient(params) - - val listener = mockk(relaxed = true) - sut.setListener(listener) - - sut.launchDropIn() - verify { listener.onDropInFailure(authError)} - } - - @Test - fun onDropInResult_notifiesListenerOfSuccess() { - val params = DropInClientParams() - .dropInRequest(dropInRequest) - val sut = DropInClient(params) - - val listener = mockk(relaxed = true) - sut.setListener(listener) - - val dropInResult = DropInResult() - dropInResult.paymentMethodNonce = - CardNonce.fromJSON(JSONObject(Fixtures.PAYMENT_METHODS_VISA_CREDIT_CARD)) - - sut.onDropInResult(dropInResult) - verify { listener.onDropInSuccess(dropInResult) } - } - - @Test - fun onDropInResult_notifiesListenerOfFailure() { - val params = DropInClientParams() - .dropInRequest(dropInRequest) - val sut = DropInClient(params) - - val listener = mockk(relaxed = true) - sut.setListener(listener) - - val dropInResult = DropInResult() - val error = Exception("error") - dropInResult.error = error - - sut.onDropInResult(dropInResult) - verify { listener.onDropInFailure(error) } - } -} diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java b/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java index c9d6233ec..f7163efeb 100644 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java +++ b/Drop-In/src/test/java/com/braintreepayments/api/DropInInternalClientUnitTest.java @@ -63,21 +63,14 @@ public void beforeEach() { @Test public void constructor_setsIntegrationTypeDropIn() { DropInInternalClient sut = - new DropInInternalClient(activity, Fixtures.TOKENIZATION_KEY, "session-id", new DropInRequest()); + new DropInInternalClient(activity, Fixtures.TOKENIZATION_KEY, new DropInRequest()); assertEquals(IntegrationType.DROP_IN, sut.braintreeClient.getIntegrationType()); } - @Test - public void internalConstructor_setsBraintreeClientWithSessionId() { - DropInInternalClient sut = - new DropInInternalClient(activity, Fixtures.TOKENIZATION_KEY, "session-id", new DropInRequest()); - assertEquals("session-id", sut.braintreeClient.getSessionId()); - } - @Test public void internalConstructor_usesDefaultBraintreeCustomUrlScheme() { DropInInternalClient sut = - new DropInInternalClient(activity, Fixtures.TOKENIZATION_KEY, "session-id", new DropInRequest()); + new DropInInternalClient(activity, Fixtures.TOKENIZATION_KEY, new DropInRequest()); assertEquals("com.braintreepayments.api.dropin.test.braintree", sut.braintreeClient.getReturnUrlScheme()); } @@ -86,7 +79,7 @@ public void internalConstructor_overridesBraintreeCustomUrlSchemeIfSet() { DropInRequest request = new DropInRequest(); request.setCustomUrlScheme("sample-custom-url-scheme"); DropInInternalClient sut = - new DropInInternalClient(activity, Fixtures.TOKENIZATION_KEY, "session-id", request); + new DropInInternalClient(activity, Fixtures.TOKENIZATION_KEY, request); assertEquals("sample-custom-url-scheme", sut.braintreeClient.getReturnUrlScheme()); } diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInLauncherUnitTest.kt b/Drop-In/src/test/java/com/braintreepayments/api/DropInLauncherUnitTest.kt new file mode 100644 index 000000000..6539cbcb8 --- /dev/null +++ b/Drop-In/src/test/java/com/braintreepayments/api/DropInLauncherUnitTest.kt @@ -0,0 +1,67 @@ +package com.braintreepayments.api + +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import junit.framework.TestCase +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DropInLauncherUnitTest : TestCase() { + + @Test + fun constructor_registersForActivityResultAndForwardsResultCallback() { + val activity = mockk(relaxed = true) + val activityResultRegistry = mockk(relaxed = true) + every { activity.activityResultRegistry } returns activityResultRegistry + + val callback = mockk(relaxed = true) + DropInLauncher(activity, callback) + + val expectedKey = "com.braintreepayments.api.DropIn.RESULT" + verify { + activityResultRegistry.register( + expectedKey, + activity, + any(), + callback + ) + } + } + + @Test + fun launch_launchesActivity() { + val activity = mockk(relaxed = true) + val activityResultRegistry = mockk(relaxed = true) + every { activity.activityResultRegistry } returns activityResultRegistry + + val activityLauncher: ActivityResultLauncher = mockk(relaxed = true) + every { + activityResultRegistry.register( + any(), + any(), + any(), + any() + ) + } returns activityLauncher + + val callback = mockk(relaxed = true) + val sut = DropInLauncher(activity, callback) + + val dropInRequest = DropInRequest() + sut.start(Fixtures.BASE64_CLIENT_TOKEN, dropInRequest) + + val slot = slot() + verify { activityLauncher.launch(capture(slot)) } + + val capturedLaunchIntent = slot.captured + assertSame(capturedLaunchIntent.dropInRequest, dropInRequest) + assertEquals(Fixtures.BASE64_CLIENT_TOKEN, capturedLaunchIntent.authorization.toString()) + } +} \ No newline at end of file diff --git a/Drop-In/src/test/java/com/braintreepayments/api/DropInLifecycleObserverUnitTest.kt b/Drop-In/src/test/java/com/braintreepayments/api/DropInLifecycleObserverUnitTest.kt deleted file mode 100644 index a9cd4def9..000000000 --- a/Drop-In/src/test/java/com/braintreepayments/api/DropInLifecycleObserverUnitTest.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.braintreepayments.api - -import androidx.activity.result.ActivityResultCallback -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.ActivityResultRegistry -import androidx.fragment.app.FragmentActivity -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import junit.framework.TestCase -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class DropInLifecycleObserverUnitTest : TestCase() { - - @Test - fun onCreate_registersForAnActivityResult() { - val activityResultRegistry = mockk(relaxed = true) - val dropInClient = mockk(relaxed = true) - - val sut = DropInLifecycleObserver(activityResultRegistry, dropInClient) - - val lifecycleOwner = FragmentActivity() - sut.onCreate(lifecycleOwner) - - val expectedKey = "com.braintreepayments.api.DropIn.RESULT" - verify { - activityResultRegistry.register( - expectedKey, - lifecycleOwner, - any(), - any() - ) - } - } - - @Test - fun onCreate_whenActivityResultReceived_forwardsResultToDropInClient() { - val activityResultRegistry = mockk(relaxed = true) - val dropInClient = mockk(relaxed = true) - - val callbackSlot = slot>() - val activityLauncher: ActivityResultLauncher = mockk(relaxed = true) - every { - activityResultRegistry.register( - any(), - any(), - any(), - capture(callbackSlot) - ) - } returns activityLauncher - - val lifecycleOwner = FragmentActivity() - val sut = DropInLifecycleObserver(activityResultRegistry, dropInClient) - sut.onCreate(lifecycleOwner) - - val dropInResult = DropInResult() - callbackSlot.captured.onActivityResult(dropInResult) - verify { dropInClient.onDropInResult(dropInResult) } - } - - @Test - fun launch_launchesActivity() { - val dropInRequest = DropInRequest() - val authorization = Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN) - - val activityResultRegistry = mockk(relaxed = true) - val dropInClient = mockk(relaxed = true) - - val activityLauncher: ActivityResultLauncher = mockk(relaxed = true) - every { - activityResultRegistry.register( - any(), - any(), - any(), - any() - ) - } returns activityLauncher - - val sut = DropInLifecycleObserver(activityResultRegistry, dropInClient) - - val lifecycleOwner = FragmentActivity() - sut.onCreate(lifecycleOwner) - - val dropInIntentData = DropInIntentData(dropInRequest, authorization, "sample-session-id") - sut.launch(dropInIntentData) - - verify { activityLauncher.launch(dropInIntentData) } - } -} \ No newline at end of file diff --git a/Drop-In/src/test/java/com/braintreepayments/api/RecentPaymentMethodsClientUnitTest.java b/Drop-In/src/test/java/com/braintreepayments/api/RecentPaymentMethodsClientUnitTest.java new file mode 100644 index 000000000..a234d0c92 --- /dev/null +++ b/Drop-In/src/test/java/com/braintreepayments/api/RecentPaymentMethodsClientUnitTest.java @@ -0,0 +1,258 @@ +package com.braintreepayments.api; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.fragment.app.FragmentActivity; +import androidx.test.core.app.ApplicationProvider; +import androidx.work.testing.WorkManagerTestInitHelper; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ActivityController; + +import java.util.ArrayList; + +@RunWith(RobolectricTestRunner.class) +public class RecentPaymentMethodsClientUnitTest { + + private FragmentActivity activity; + + private Context context; + private DropInSharedPreferences dropInSharedPreferences; + + @Before + public void beforeEach() { + context = ApplicationProvider.getApplicationContext(); + dropInSharedPreferences = mock(DropInSharedPreferences.class); + + ActivityController activityController = + Robolectric.buildActivity(FragmentActivity.class); + activity = activityController.get(); + + // This suppresses errors from WorkManager initialization within BraintreeClient initialization (AnalyticsClient) + WorkManagerTestInitHelper.initializeTestWorkManager(context); + } + + @Test + public void fetchMostRecentPaymentMethod_forwardsAuthorizationFetchErrors() { + Exception authError = new Exception("auth error"); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationError(authError) + .build(); + + RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient( + braintreeClient, + new GooglePayClient(braintreeClient), + new PaymentMethodClient(braintreeClient), + dropInSharedPreferences + ); + + FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); + sut.fetchMostRecentPaymentMethod(activity, callback); + + verify(callback).onResult(null, authError); + } + + @Test + public void fetchMostRecentPaymentMethod_callsBackWithErrorIfInvalidClientTokenWasUsed() { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationSuccess(Authorization.fromString(Fixtures.TOKENIZATION_KEY)) + .build(); + + RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient( + braintreeClient, + new GooglePayClient(braintreeClient), + new PaymentMethodClient(braintreeClient), + dropInSharedPreferences + ); + + FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); + sut.fetchMostRecentPaymentMethod(activity, callback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(InvalidArgumentException.class); + verify(callback).onResult((DropInResult) isNull(), captor.capture()); + + InvalidArgumentException exception = captor.getValue(); + assertEquals("DropInClient#fetchMostRecentPaymentMethods() must be called with a client token", exception.getMessage()); + } + + @Test + public void fetchMostRecentPaymentMethod_callsBackWithResultIfLastUsedPaymentMethodTypeWasPayWithGoogle() throws JSONException, JSONException { + + GooglePayClient googlePayClient = new MockGooglePayClientBuilder() + .isReadyToPaySuccess(true) + .build(); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) + .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) + .build(); + + RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient( + braintreeClient, + googlePayClient, + new PaymentMethodClient(braintreeClient), + dropInSharedPreferences + ); + + when( + dropInSharedPreferences.getLastUsedPaymentMethod() + ).thenReturn(DropInPaymentMethod.GOOGLE_PAY); + + FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); + sut.fetchMostRecentPaymentMethod(activity, callback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); + verify(callback).onResult(captor.capture(), (Exception) isNull()); + + DropInResult result = captor.getValue(); + assertEquals(DropInPaymentMethod.GOOGLE_PAY, result.getPaymentMethodType()); + assertNull(result.getPaymentMethodNonce()); + } + + @Test + public void fetchMostRecentPaymentMethod_doesNotCallBackWithPayWithGoogleIfPayWithGoogleIsNotAvailable() throws JSONException { + + GooglePayClient googlePayClient = new MockGooglePayClientBuilder() + .isReadyToPaySuccess(false) + .build(); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) + .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) + .build(); + + ArrayList paymentMethods = new ArrayList<>(); + paymentMethods.add(CardNonce.fromJSON(new JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE))); + paymentMethods.add(GooglePayCardNonce.fromJSON(new JSONObject(Fixtures.GOOGLE_PAY_NETWORK_TOKENIZED_RESPONSE))); + PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() + .getPaymentMethodNoncesSuccess(paymentMethods) + .build(); + + when( + dropInSharedPreferences.getLastUsedPaymentMethod() + ).thenReturn(DropInPaymentMethod.GOOGLE_PAY); + + RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient( + braintreeClient, + googlePayClient, + paymentMethodClient, + dropInSharedPreferences + ); + + FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); + sut.fetchMostRecentPaymentMethod(activity, callback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); + verify(callback).onResult(captor.capture(), (Exception) isNull()); + + DropInResult result = captor.getValue(); + assertEquals(DropInPaymentMethod.VISA, result.getPaymentMethodType()); + assertNotNull(result.getPaymentMethodNonce()); + assertEquals("11", ((CardNonce) result.getPaymentMethodNonce()).getLastTwo()); + } + + @Test + public void fetchMostRecentPaymentMethod_callsBackWithErrorOnGetPaymentMethodsError() { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) + .build(); + + PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() + .getPaymentMethodNoncesError(new BraintreeException("Error occurred")) + .build(); + + RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient( + braintreeClient, + new GooglePayClient(braintreeClient), + paymentMethodClient, + dropInSharedPreferences + ); + + FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); + sut.fetchMostRecentPaymentMethod(activity, callback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BraintreeException.class); + verify(callback).onResult((DropInResult) isNull(), captor.capture()); + + BraintreeException exception = captor.getValue(); + assertEquals("Error occurred", exception.getMessage()); + } + + @Test + public void fetchMostRecentPaymentMethod_callsBackWithResultWhenThereIsAPaymentMethod() + throws JSONException { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) + .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) + .build(); + + ArrayList paymentMethods = new ArrayList<>(); + paymentMethods.add(CardNonce.fromJSON(new JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE))); + + PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() + .getPaymentMethodNoncesSuccess(paymentMethods) + .build(); + + RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient( + braintreeClient, + new GooglePayClient(braintreeClient), + paymentMethodClient, + dropInSharedPreferences + ); + + FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); + sut.fetchMostRecentPaymentMethod(activity, callback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); + verify(callback).onResult(captor.capture(), (Exception) isNull()); + + DropInResult result = captor.getValue(); + assertEquals(DropInPaymentMethod.VISA, result.getPaymentMethodType()); + assertNotNull(result.getPaymentMethodNonce()); + assertEquals("11", ((CardNonce) result.getPaymentMethodNonce()).getLastTwo()); + } + + @Test + public void fetchMostRecentPaymentMethod_callsBackWithNullResultWhenThereAreNoPaymentMethods() throws JSONException { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) + .configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY)) + .build(); + + ArrayList paymentMethods = new ArrayList<>(); + + PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder() + .getPaymentMethodNoncesSuccess(paymentMethods) + .build(); + + RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient( + braintreeClient, + new GooglePayClient(braintreeClient), + paymentMethodClient, + dropInSharedPreferences + ); + + FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class); + sut.fetchMostRecentPaymentMethod(activity, callback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DropInResult.class); + verify(callback).onResult(captor.capture(), (Exception) isNull()); + + DropInResult result = captor.getValue(); + assertNull(result.getPaymentMethodType()); + assertNull(result.getPaymentMethodNonce()); + } +}