Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,86 @@
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;

class AuthTabInternalClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

For newer classes, we could use kotlin probably?

Copy link
Author

Choose a reason for hiding this comment

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

yes, I was thinking the same thing, I will rewrite it


private final AuthTabIntent.Builder authTabIntentBuilder;
private final CustomTabsIntent.Builder customTabsIntentBuilder;

AuthTabInternalClient() {
this(new AuthTabIntent.Builder(), new CustomTabsIntent.Builder());
}

@VisibleForTesting
AuthTabInternalClient(AuthTabIntent.Builder authTabBuilder,
CustomTabsIntent.Builder customTabsBuilder) {
this.authTabIntentBuilder = authTabBuilder;
this.customTabsIntentBuilder = customTabsBuilder;
}

/**
* Checks if Auth Tab is supported by the current browser
*/
boolean isAuthTabSupported(Context context) {
String packageName = CustomTabsClient.getPackageName(context, null);
if (packageName == null) {
return false;
}
return CustomTabsClient.isAuthTabSupported(context, packageName);
}

/**
* Launch URL using Auth Tab if supported, otherwise fall back to Custom Tabs
*/
void launchUrl(Context context,
Uri url,
String returnUrlScheme,
Uri appLinkUri,
ActivityResultLauncher<Intent> launcher,
LaunchType launchType) throws ActivityNotFoundException {

if (launcher != null && isAuthTabSupported(context)) {
// Auth Tab flow
AuthTabIntent authTabIntent = authTabIntentBuilder.build();

if (appLinkUri != null) {
// For app links (HTTPS), extract host and path
String host = appLinkUri.getHost();
String path = appLinkUri.getPath();
if (path == null) {
path = "/";
}
assert host != null;
authTabIntent.launch(launcher, url, host, path);
} else if (returnUrlScheme != null) {
// For custom schemes
authTabIntent.launch(launcher, url, returnUrlScheme);
} else {
throw new IllegalArgumentException("Either returnUrlScheme or appLinkUri must be provided");
}
} else {
// Chrome Custom Tabs Fallback
CustomTabsIntent customTabsIntent = customTabsIntentBuilder.build();

if (launchType != null) {
switch (launchType) {
case ACTIVITY_NEW_TASK:
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
break;
case ACTIVITY_CLEAR_TOP:
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
break;
}
}
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
Loading