Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.braintreepayments.api

/**
* Callback interface for Auth Tab results
*/
fun interface AuthTabCallback {
/**
* @param result The final result of the browser switch operation
*/
fun onResult(result: BrowserSwitchFinalResult)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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

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


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 = isAuthTabSupported(context)

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

if (launchType == LaunchType.ACTIVITY_CLEAR_TOP) {
authTabIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
appLinkUri?.host?.let { host ->
val path = appLinkUri.path ?: "/"
authTabIntent.launch(launcher, url, host, path)
} ?: returnUrlScheme?.let {
authTabIntent.launch(launcher, url, returnUrlScheme)
} ?: throw IllegalArgumentException("Either returnUrlScheme or appLinkUri must be provided")
} else {
// fall back 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,76 @@
public class BrowserSwitchClient {

private final BrowserSwitchInspector browserSwitchInspector;

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

/**
* 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.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;
}
callback.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 +101,53 @@ 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
);

boolean useAuthTab = authTabInternalClient.isAuthTabSupported(activity);

if (useAuthTab) {
this.pendingAuthTabRequest = request;
}

authTabInternalClient.launchUrl(
activity,
browserSwitchUrl,
returnUrlScheme,
appLinkUri,
authTabLauncher,
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,20 +188,15 @@ 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) {
if (intent.getData() != null) {
Uri returnUrl = intent.getData();

try {
Expand All @@ -149,4 +211,8 @@ public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull
}
return BrowserSwitchFinalResult.NoResult.INSTANCE;
}
}

public boolean isAuthTabSupported(Context context) {
return authTabInternalClient.isAuthTabSupported(context);
}
}

This file was deleted.

Loading