Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# browser-switch-android Release Notes

## 3.1.1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be 3.1.1 or Unreleased? Not sure how it works for feature branches, but doesn't the release process automatically update to the correct release version?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tdchow you might know the answer to this a bit better

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah that's a great catch. When we release the SDK, the release script will replace unreleased with the version number. So we'll need change the 3.3.1 to unreleased (lowercase unreleased - I think our script looks for that exact string).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thank you both for catching this. It is now addressed in 84cf245


* Add AuthTab Support
* Upgrade `androidx.browser:browser` dependency version to 1.9.0
* Upgrade `compileSdkVersion` and `targetSdkVersion` to API 36
* Replace `ChromeCustomTabsInternalClient.java` with `AuthTabInternalClient.kt`
* Add parameterized constructor `BrowserSwitchClient(ActivityResultCaller)` to initialize AuthTab support
* Maintain default constructor `BrowserSwitchClient()` (without AuthTab support) for backward compatibility

## 3.1.0

* Add `LaunchType` to `BrowserSwitchOptions` to specify how the browser switch should be launched
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.braintreepayments.api;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
Expand Down Expand Up @@ -34,7 +33,7 @@ public class BrowserSwitchClient {
/**
* Construct a client that manages browser switching with Chrome Custom Tabs fallback only.
* This constructor does not initialize Auth Tab support. For Auth Tab functionality,
* use {@link #BrowserSwitchClient(Activity)} instead.
* use {@link #BrowserSwitchClient(ActivityResultCaller)} instead.
*/
public BrowserSwitchClient() {
this(new BrowserSwitchInspector(), new AuthTabInternalClient());
Expand All @@ -47,8 +46,9 @@ public BrowserSwitchClient() {
* <p>IMPORTANT: This constructor enables the AuthTab functionality, which has several caveats:
*
* <ul>
* <li>The provided activity MUST implement {@link ActivityResultCaller}, which is true for all
* instances of {@link androidx.activity.ComponentActivity}.
* <li><strong>This constructor must be called in the activity/fragment's {@code onCreate()} method</strong>
* to properly register the activity result launcher before the activity/fragment is started.
* <li>The caller must be an {@link ActivityResultCaller} to register for activity results.
* <li>{@link LaunchType#ACTIVITY_NEW_TASK} is not supported when using AuthTab and will be ignored.
* Only {@link LaunchType#ACTIVITY_CLEAR_TOP} is supported with AuthTab.
* <li>When using SingleTop activities, you must check for launcher results in {@code onResume()} as well
Expand All @@ -66,38 +66,38 @@ public BrowserSwitchClient() {
* <p>Consider using the default constructor {@link #BrowserSwitchClient()} if these limitations
* are incompatible with your implementation.
*
* @param activity The activity used to initialize the Auth Tab launcher. Must implement
* {@link ActivityResultCaller}.
* @param caller The ActivityResultCaller used to initialize the Auth Tab launcher.
*/
public BrowserSwitchClient(@NonNull Activity activity) {
public BrowserSwitchClient(@NonNull ActivityResultCaller caller) {
this(new BrowserSwitchInspector(), new AuthTabInternalClient());
initializeAuthTabLauncher(activity);
initializeAuthTabLauncher(caller);
}

@VisibleForTesting
BrowserSwitchClient(BrowserSwitchInspector browserSwitchInspector,
AuthTabInternalClient authTabInternalClient) {
this.browserSwitchInspector = browserSwitchInspector;
this.authTabInternalClient = authTabInternalClient;
this.authTabCallbackResult = null;
}

@VisibleForTesting
BrowserSwitchClient(@NonNull ActivityResultCaller caller,
BrowserSwitchInspector inspector,
AuthTabInternalClient internal) {
this(inspector, internal);
initializeAuthTabLauncher(caller);
}

/**
* Initialize the Auth Tab launcher. This should be called in the activity's onCreate()
* before the activity is started.
* Initialize the Auth Tab launcher. This should be called in the activity/fragment's onCreate()
* before it is started.
*
* @param activity The activity used to initialize the Auth Tab launcher
* @param caller The ActivityResultCaller (Activity or Fragment) used to initialize the Auth Tab launcher
*/
public void initializeAuthTabLauncher(@NonNull Activity activity) {

if (!(activity instanceof ActivityResultCaller)) {
return;
}

ComponentActivity componentActivity = (ComponentActivity) activity;
private void initializeAuthTabLauncher(@NonNull ActivityResultCaller caller) {

this.authTabLauncher = AuthTabIntent.registerActivityResultLauncher(
componentActivity,
caller,
result -> {
BrowserSwitchFinalResult finalResult;
switch (result.resultCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.browser.auth.AuthTabIntent;

Expand Down Expand Up @@ -252,9 +253,11 @@ public void initializeAuthTabLauncher_registersLauncherWithActivity() {
any(ActivityResultCallback.class)
)).thenReturn(mockLauncher);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, authTabInternalClient);

sut.initializeAuthTabLauncher(componentActivity);
BrowserSwitchClient sut = new BrowserSwitchClient(
componentActivity,
browserSwitchInspector,
authTabInternalClient
);

mockedAuthTab.verify(() -> AuthTabIntent.registerActivityResultLauncher(
eq(componentActivity),
Expand All @@ -280,27 +283,30 @@ public void start_withAuthTabLauncherInitialized_usesPendingAuthTabRequest() thr
"return-url-scheme"
)).thenReturn(true);
when(authTabInternalClient.isAuthTabSupported(componentActivity)).thenReturn(true);

ArgumentCaptor<ActivityResultCallback> callbackCaptor =
ArgumentCaptor<ActivityResultCallback<AuthTabIntent.AuthResult>> callbackCaptor =
ArgumentCaptor.forClass(ActivityResultCallback.class);
mockedAuthTab.when(() -> AuthTabIntent.registerActivityResultLauncher(
eq(componentActivity),
callbackCaptor.capture()
)).thenReturn(mockLauncher);
BrowserSwitchClient sut = new BrowserSwitchClient(
componentActivity,
browserSwitchInspector,
authTabInternalClient
);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, authTabInternalClient);

sut.initializeAuthTabLauncher(componentActivity);
mockedAuthTab.verify(() -> AuthTabIntent.registerActivityResultLauncher(
eq(componentActivity),
any(ActivityResultCallback.class)
));

JSONObject metadata = new JSONObject();
BrowserSwitchOptions options = new BrowserSwitchOptions()
.requestCode(123)
.url(browserSwitchDestinationUrl)
.returnUrlScheme("return-url-scheme")
.metadata(metadata);

BrowserSwitchStartResult result = sut.start(componentActivity, options);

assertTrue(result instanceof BrowserSwitchStartResult.Started);

verify(authTabInternalClient).launchUrl(
Expand All @@ -314,10 +320,6 @@ public void start_withAuthTabLauncherInitialized_usesPendingAuthTabRequest() thr

String pendingRequestString = ((BrowserSwitchStartResult.Started) result).getPendingRequest();
assertNotNull(pendingRequestString);

BrowserSwitchRequest decodedRequest = BrowserSwitchRequest.fromBase64EncodedJSON(pendingRequestString);
assertEquals(123, decodedRequest.getRequestCode());
assertEquals(browserSwitchDestinationUrl, decodedRequest.getUrl());
}
}

Expand All @@ -327,27 +329,33 @@ public void authTabCallback_withResultOK_setsInternalCallbackResult() {

ArgumentCaptor<ActivityResultCallback<AuthTabIntent.AuthResult>> callbackCaptor =
ArgumentCaptor.forClass(ActivityResultCallback.class);
mockedAuthTab.when(() -> AuthTabIntent.registerActivityResultLauncher(
eq(componentActivity),
callbackCaptor.capture()
)).thenReturn(mockLauncher);

when(browserSwitchInspector.isDeviceConfiguredForDeepLinking(
componentActivity.getApplicationContext(),
"return-url-scheme"
)).thenReturn(true);
when(authTabInternalClient.isAuthTabSupported(componentActivity)).thenReturn(true);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, authTabInternalClient);
sut.initializeAuthTabLauncher(componentActivity);
mockedAuthTab.when(() -> AuthTabIntent.registerActivityResultLauncher(
eq(componentActivity),
callbackCaptor.capture()
)).thenReturn(mockLauncher);

BrowserSwitchClient sut = new BrowserSwitchClient(
componentActivity,
browserSwitchInspector,
authTabInternalClient
);

JSONObject metadata = new JSONObject();
BrowserSwitchOptions options = new BrowserSwitchOptions()
.requestCode(123)
.url(browserSwitchDestinationUrl)
.returnUrlScheme("return-url-scheme")
.metadata(metadata);
sut.start(componentActivity, options);

BrowserSwitchStartResult startResult = sut.start(componentActivity, options);
String pendingRequest = ((BrowserSwitchStartResult.Started) startResult).getPendingRequest();

Uri resultUri = Uri.parse("return-url-scheme://success");
AuthTabIntent.AuthResult mockAuthResult = mock(AuthTabIntent.AuthResult.class, withSettings()
Expand All @@ -357,7 +365,7 @@ public void authTabCallback_withResultOK_setsInternalCallbackResult() {
callbackCaptor.getValue().onActivityResult(mockAuthResult);

Intent dummyIntent = new Intent();
BrowserSwitchFinalResult capturedResult = sut.completeRequest(dummyIntent, "dummyPendingRequest");
BrowserSwitchFinalResult capturedResult = sut.completeRequest(dummyIntent, pendingRequest);
assertTrue(capturedResult instanceof BrowserSwitchFinalResult.Success);

BrowserSwitchFinalResult.Success successResult =
Expand All @@ -377,9 +385,11 @@ public void authTabCallback_withResultCanceled_callsCallbackWithNoResult() {
callbackCaptor.capture()
)).thenReturn(mockLauncher);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, authTabInternalClient);

sut.initializeAuthTabLauncher(componentActivity);
BrowserSwitchClient sut = new BrowserSwitchClient(
componentActivity,
browserSwitchInspector,
authTabInternalClient
);

AuthTabIntent.AuthResult mockAuthResult = mock(AuthTabIntent.AuthResult.class, withSettings()
.useConstructor(AuthTabIntent.RESULT_CANCELED, null)
Expand All @@ -395,7 +405,6 @@ public void authTabCallback_withResultCanceled_callsCallbackWithNoResult() {

@Test
public void start_withoutAuthTabLauncher_fallsBackToCustomTabs() {

when(browserSwitchInspector.isDeviceConfiguredForDeepLinking(
componentActivity.getApplicationContext(),
"return-url-scheme"
Expand Down Expand Up @@ -431,7 +440,6 @@ public void start_whenAuthTabLauncherIsNull_fallsBackToCustomTabs() {
"return-url-scheme"
)).thenReturn(true);

// Explicitly ensure AuthTab is supported but we still fallback due to null launcher
when(authTabInternalClient.isAuthTabSupported(componentActivity)).thenReturn(true);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, authTabInternalClient);
Expand Down Expand Up @@ -479,10 +487,11 @@ public void isAuthTabSupported_returnsTrueWhenLauncherInitialized() {
any(ActivityResultCallback.class)
)).thenReturn(mockLauncher);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector,
authTabInternalClient);

sut.initializeAuthTabLauncher(componentActivity);
BrowserSwitchClient sut = new BrowserSwitchClient(
componentActivity,
browserSwitchInspector,
authTabInternalClient
);

boolean result = sut.isAuthTabSupported(applicationContext);

Expand All @@ -494,39 +503,35 @@ public void isAuthTabSupported_returnsTrueWhenLauncherInitialized() {
@Test
public void isAuthTabSupported_returnsFalseWhenBrowserDoesNotSupportAuthTab() {
try (MockedStatic<AuthTabIntent> mockedAuthTab = mockStatic(AuthTabIntent.class)) {
when(authTabInternalClient.isAuthTabSupported(applicationContext)).thenReturn(false);

mockedAuthTab.when(() -> AuthTabIntent.registerActivityResultLauncher(
any(ComponentActivity.class),
any(ActivityResultCaller.class),
any(ActivityResultCallback.class)
)).thenReturn(mockLauncher);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector,
authTabInternalClient);
when(authTabInternalClient.isAuthTabSupported(any())).thenReturn(false);

sut.initializeAuthTabLauncher(componentActivity);
BrowserSwitchClient sut = new BrowserSwitchClient(
componentActivity,
browserSwitchInspector,
authTabInternalClient
);

boolean result = sut.isAuthTabSupported(applicationContext);
boolean result = sut.isAuthTabSupported(componentActivity);

assertFalse(result);
verify(authTabInternalClient).isAuthTabSupported(applicationContext);
}
}

@Test
public void parameterizedConstructor_initializesAuthTabLauncher() {
public void defaultConstructor_doesNotInitializeAuthTabLauncher() {
try (MockedStatic<AuthTabIntent> mockedAuthTab = mockStatic(AuthTabIntent.class)) {
mockedAuthTab.when(() -> AuthTabIntent.registerActivityResultLauncher(
any(ComponentActivity.class),
any(ActivityResultCallback.class)
)).thenReturn(mockLauncher);
BrowserSwitchClient sut = new BrowserSwitchClient();

BrowserSwitchClient sut = new BrowserSwitchClient(componentActivity);
mockedAuthTab.verifyNoInteractions();

mockedAuthTab.verify(() -> AuthTabIntent.registerActivityResultLauncher(
eq(componentActivity),
any(ActivityResultCallback.class)
));
when(authTabInternalClient.isAuthTabSupported(applicationContext)).thenReturn(true);
boolean result = sut.isAuthTabSupported(applicationContext);
assertFalse(result);
}
}

Expand Down