Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.braintreepayments.api

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.VisibleForTesting
import androidx.browser.auth.AuthTabIntent
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent

@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
class AuthTabInternalClient @VisibleForTesting constructor(
private val authTabIntentBuilder: AuthTabIntent.Builder,
private val customTabsIntentBuilder: CustomTabsIntent.Builder
) {

constructor() : this(AuthTabIntent.Builder(), CustomTabsIntent.Builder())

/**
* Checks if Auth Tab is supported by the current browser
*/
fun isAuthTabSupported(context: Context): Boolean {
val packageName = CustomTabsClient.getPackageName(context, null)
return when (packageName) {
null -> false
else -> CustomTabsClient.isAuthTabSupported(context, packageName)
}
}

/**
* Launch URL using Auth Tab if supported, otherwise fall back to Custom Tabs
*/
@Throws(ActivityNotFoundException::class)
fun launchUrl(
context: Context,
url: Uri,
returnUrlScheme: String?,
appLinkUri: Uri?,
launcher: ActivityResultLauncher<Intent>?,
launchType: LaunchType?
) {
val useAuthTab = launcher != null &&
isAuthTabSupported(context) &&
(returnUrlScheme != null || appLinkUri?.host != null)

if (useAuthTab) {
val authTabIntent = authTabIntentBuilder.build()

if (launchType == LaunchType.ACTIVITY_CLEAR_TOP) {
authTabIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}

when {
appLinkUri?.host != null -> {
val host = appLinkUri.host!!
val path = appLinkUri.path ?: "/"
authTabIntent.launch(launcher!!, url, host, path)
}
returnUrlScheme != null -> {
authTabIntent.launch(launcher!!, url, returnUrlScheme)
}
}
} else {
//fallback to Custom Tabs
launchCustomTabs(context, url, launchType)
}
}
private fun launchCustomTabs(context: Context, url: Uri, launchType: LaunchType?) {
val customTabsIntent = customTabsIntentBuilder.build()
when (launchType) {
LaunchType.ACTIVITY_NEW_TASK -> {
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
LaunchType.ACTIVITY_CLEAR_TOP -> {
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
null -> { }
}
customTabsIntent.launchUrl(context, url)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import android.net.Uri;

import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.browser.auth.AuthTabIntent;

import com.braintreepayments.api.browserswitch.R;

Expand All @@ -19,35 +21,81 @@
public class BrowserSwitchClient {

private final BrowserSwitchInspector browserSwitchInspector;

private final ChromeCustomTabsInternalClient customTabsInternalClient;
private final AuthTabInternalClient authTabInternalClient;
private ActivityResultLauncher<Intent> authTabLauncher;
private BrowserSwitchRequest pendingAuthTabRequest;
private AuthTabCallback authTabCallback;

/**
* Construct a client that manages the logic for browser switching.
*/
public BrowserSwitchClient() {
this(new BrowserSwitchInspector(), new ChromeCustomTabsInternalClient());
this(new BrowserSwitchInspector(), new AuthTabInternalClient());
}

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

/**
* Initialize the Auth Tab launcher. This should be called in the activity's onCreate()
* before the activity is started.
*/
public void initializeAuthTabLauncher(@NonNull ComponentActivity activity,
@NonNull AuthTabCallback callback) {
this.authTabCallback = callback;
this.authTabLauncher = AuthTabIntent.registerActivityResultLauncher(
activity,
result -> {
BrowserSwitchFinalResult finalResult;

switch (result.resultCode) {
case AuthTabIntent.RESULT_OK:
if (result.resultUri != null && pendingAuthTabRequest != null) {
finalResult = new BrowserSwitchFinalResult.Success(
result.resultUri,
pendingAuthTabRequest
);
} else {
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
}
break;
case AuthTabIntent.RESULT_CANCELED:
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
break;
case AuthTabIntent.RESULT_VERIFICATION_FAILED:
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
break;
case AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT:
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
break;
default:
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
}

if (this.authTabCallback != null) {
this.authTabCallback.onResult(finalResult);
}
pendingAuthTabRequest = null;
}
);
}

/**
* Open a browser or <a href="https://developer.chrome.com/multidevice/android/customtabs">Chrome Custom Tab</a>
* with a given set of {@link BrowserSwitchOptions} from an Android activity.
* Open a browser or Auth Tab with a given set of {@link BrowserSwitchOptions} from an Android activity.
*
* @param activity the activity used to start browser switch
* @param browserSwitchOptions {@link BrowserSwitchOptions} the options used to configure the browser switch
* @return a {@link BrowserSwitchStartResult.Started} that should be stored and passed to
* {@link BrowserSwitchClient#completeRequest(Intent, String)} upon return to the app,
* {@link BrowserSwitchClient#completeRequest(Intent, String)} upon return to the app (for Custom Tabs fallback),
* or {@link BrowserSwitchStartResult.Failure} if browser could not be launched.
*/
@NonNull
public BrowserSwitchStartResult start(@NonNull ComponentActivity activity, @NonNull BrowserSwitchOptions browserSwitchOptions) {
public BrowserSwitchStartResult start(@NonNull ComponentActivity activity,
@NonNull BrowserSwitchOptions browserSwitchOptions) {
try {
assertCanPerformBrowserSwitch(activity, browserSwitchOptions);
} catch (BrowserSwitchException e) {
Expand All @@ -58,29 +106,57 @@ public BrowserSwitchStartResult start(@NonNull ComponentActivity activity, @NonN
int requestCode = browserSwitchOptions.getRequestCode();
String returnUrlScheme = browserSwitchOptions.getReturnUrlScheme();
Uri appLinkUri = browserSwitchOptions.getAppLinkUri();

JSONObject metadata = browserSwitchOptions.getMetadata();

if (activity.isFinishing()) {
String activityFinishingMessage =
"Unable to start browser switch while host Activity is finishing.";
return new BrowserSwitchStartResult.Failure(new BrowserSwitchException(activityFinishingMessage));
} else {
LaunchType launchType = browserSwitchOptions.getLaunchType();
BrowserSwitchRequest request;
try {
request = new BrowserSwitchRequest(
requestCode,
browserSwitchUrl,
metadata,
returnUrlScheme,
appLinkUri
);
customTabsInternalClient.launchUrl(activity, browserSwitchUrl, launchType);
return new BrowserSwitchStartResult.Started(request.toBase64EncodedJSON());
} catch (ActivityNotFoundException | BrowserSwitchException e) {
return new BrowserSwitchStartResult.Failure(new BrowserSwitchException("Unable to start browser switch without a web browser.", e));
}

LaunchType launchType = browserSwitchOptions.getLaunchType();
BrowserSwitchRequest request;

try {
request = new BrowserSwitchRequest(
requestCode,
browserSwitchUrl,
metadata,
returnUrlScheme,
appLinkUri
);

// Check if we should use Auth Tab
boolean useAuthTab = authTabLauncher != null &&
authTabInternalClient.isAuthTabSupported(activity);

if (useAuthTab) {
// Store the pending request for Auth Tab callback
this.pendingAuthTabRequest = request;
}

// Launch using Auth Tab or Custom Tabs
authTabInternalClient.launchUrl(
activity,
browserSwitchUrl,
returnUrlScheme,
appLinkUri,
authTabLauncher, // Will be null if not initialized or not supported
launchType
);

return new BrowserSwitchStartResult.Started(request.toBase64EncodedJSON());

} catch (ActivityNotFoundException e) {
this.pendingAuthTabRequest = null;
return new BrowserSwitchStartResult.Failure(
new BrowserSwitchException("Unable to start browser switch without a web browser.", e)
);
} catch (Exception e) {
this.pendingAuthTabRequest = null;
return new BrowserSwitchStartResult.Failure(
new BrowserSwitchException("Unable to start browser switch: " + e.getMessage(), e)
);
}
}

Expand Down Expand Up @@ -121,17 +197,12 @@ private boolean isValidRequestCode(int requestCode) {
}

/**
* Completes the browser switch flow and returns a browser switch result if a match is found for
* the given {@link BrowserSwitchRequest}
* Completes the browser switch flow for Custom Tabs fallback scenarios.
* This method is still needed for devices that don't support Auth Tab.
*
* @param intent the intent to return to your application containing a deep link result from the
* browser flow
* @param pendingRequest the pending request string returned from {@link BrowserSwitchStartResult.Started} via
* {@link BrowserSwitchClient#start(ComponentActivity, BrowserSwitchOptions)}
* @return a {@link BrowserSwitchFinalResult.Success} if the browser switch was successfully
* completed, or {@link BrowserSwitchFinalResult.NoResult} if no result can be found for the given
* pending request String. A {@link BrowserSwitchFinalResult.NoResult} will be
* returned if the user returns to the app without completing the browser switch flow.
* @param intent the intent to return to your application containing a deep link result
* @param pendingRequest the pending request string returned from {@link BrowserSwitchStartResult.Started}
* @return a {@link BrowserSwitchFinalResult}
*/
public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull String pendingRequest) {
if (intent != null && intent.getData() != null) {
Expand All @@ -149,4 +220,18 @@ public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull
}
return BrowserSwitchFinalResult.NoResult.INSTANCE;
}
}

/**
* Check if Auth Tab is supported on the current device
*/
public boolean isAuthTabSupported(Context context) {
return authTabInternalClient.isAuthTabSupported(context);
}

/**
* Callback interface for Auth Tab results
*/
public interface AuthTabCallback {
void onResult(BrowserSwitchFinalResult result);
}
}

This file was deleted.

Loading