Skip to content

Commit cc39a8a

Browse files
noguiersaperi22
andauthored
Completed AuthTab Implementation (#121)
* remove duplicate intent-filter * - Update androidx.browser to 1.9.0 - Update AGP version to 8.9.1 -- requires update to gradle wrapper 8.11.1 - Update compile and target sdk to 36 * first iteration on AuthTab Setup: added AuthTabInternalClient, removed ChromeCustomTabsInternalClient, modified ComposeActivity & DemoActivitySingleTop to accept the above changes, added toast visuals to see whether or not the AuthTab is supported * Rename .java to .kt * second iteration on AuthTab Setup: rewrote AuthTabInternalClient to Kotlin and added a ClearTop Flag, wrote new AuthTabInternalClientUnitTest, removed ChromeCustomTabsInternalClientUnitTest, modified BrowserSwitchClientUnitTest to accept the AuthTab changes * third iteration on AuthTab Setup: removed CustomTabIntentBuilder, removed tests associated with CustomTabs from AuthTabInternalClientUnitTest,, modified BrowserSwitchClientUnitTest to accept the AuthTab launchURL signature * EOD commit: -Testing the authtab flow on older devices without intents * Revert "EOD commit:" This reverts commit e27cfa1. * Revert "third iteration on AuthTab Setup:" This reverts commit 9803e43. Branching still needed to provide support for older devices. * Completed implementation of AuthTab Support: - added AuthTabInternalClient, - removed ChromeCustomTabsInternalClient, - modified ComposeActivity & DemoActivitySingleTop to accept the above changes, - added toast visuals to see whether or not the AuthTab is supported * Address PR suggestions * Address PR suggestions --------- Co-authored-by: saperi <[email protected]>
1 parent ee1cce3 commit cc39a8a

File tree

10 files changed

+794
-194
lines changed

10 files changed

+794
-194
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.braintreepayments.api
2+
3+
/**
4+
* Callback interface for Auth Tab results
5+
*/
6+
fun interface AuthTabCallback {
7+
/**
8+
* @param result The final result of the browser switch operation
9+
*/
10+
fun onResult(result: BrowserSwitchFinalResult)
11+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.braintreepayments.api
2+
3+
import android.content.ActivityNotFoundException
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.net.Uri
7+
import androidx.activity.result.ActivityResultLauncher
8+
import androidx.browser.auth.AuthTabIntent
9+
import androidx.browser.customtabs.CustomTabsClient
10+
import androidx.browser.customtabs.CustomTabsIntent
11+
12+
internal class AuthTabInternalClient (
13+
private val authTabIntentBuilder: AuthTabIntent.Builder = AuthTabIntent.Builder(),
14+
private val customTabsIntentBuilder: CustomTabsIntent.Builder = CustomTabsIntent.Builder()
15+
) {
16+
17+
fun isAuthTabSupported(context: Context): Boolean {
18+
val packageName = CustomTabsClient.getPackageName(context, null)
19+
return when (packageName) {
20+
null -> false
21+
else -> CustomTabsClient.isAuthTabSupported(context, packageName)
22+
}
23+
}
24+
25+
/**
26+
* Launch URL using Auth Tab if supported, otherwise fall back to Custom Tabs
27+
*/
28+
@Throws(ActivityNotFoundException::class)
29+
fun launchUrl(
30+
context: Context,
31+
url: Uri,
32+
returnUrlScheme: String?,
33+
appLinkUri: Uri?,
34+
launcher: ActivityResultLauncher<Intent>,
35+
launchType: LaunchType?
36+
) {
37+
val useAuthTab = isAuthTabSupported(context)
38+
39+
if (useAuthTab) {
40+
val authTabIntent = authTabIntentBuilder.build()
41+
42+
if (launchType == LaunchType.ACTIVITY_CLEAR_TOP) {
43+
authTabIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
44+
}
45+
appLinkUri?.host?.let { host ->
46+
val path = appLinkUri.path ?: "/"
47+
authTabIntent.launch(launcher, url, host, path)
48+
} ?: returnUrlScheme?.let {
49+
authTabIntent.launch(launcher, url, returnUrlScheme)
50+
} ?: throw IllegalArgumentException("Either returnUrlScheme or appLinkUri must be provided")
51+
} else {
52+
// fall back to Custom Tabs
53+
launchCustomTabs(context, url, launchType)
54+
}
55+
}
56+
private fun launchCustomTabs(context: Context, url: Uri, launchType: LaunchType?) {
57+
val customTabsIntent = customTabsIntentBuilder.build()
58+
when (launchType) {
59+
LaunchType.ACTIVITY_NEW_TASK -> {
60+
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
61+
}
62+
LaunchType.ACTIVITY_CLEAR_TOP -> {
63+
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
64+
}
65+
null -> { }
66+
}
67+
customTabsIntent.launchUrl(context, url)
68+
}
69+
}

browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import android.net.Uri;
77

88
import androidx.activity.ComponentActivity;
9+
import androidx.activity.result.ActivityResultLauncher;
910
import androidx.annotation.NonNull;
1011
import androidx.annotation.VisibleForTesting;
12+
import androidx.browser.auth.AuthTabIntent;
1113

1214
import com.braintreepayments.api.browserswitch.R;
1315

@@ -19,35 +21,76 @@
1921
public class BrowserSwitchClient {
2022

2123
private final BrowserSwitchInspector browserSwitchInspector;
22-
23-
private final ChromeCustomTabsInternalClient customTabsInternalClient;
24+
private final AuthTabInternalClient authTabInternalClient;
25+
private ActivityResultLauncher<Intent> authTabLauncher;
26+
private BrowserSwitchRequest pendingAuthTabRequest;
2427

2528
/**
2629
* Construct a client that manages the logic for browser switching.
2730
*/
2831
public BrowserSwitchClient() {
29-
this(new BrowserSwitchInspector(), new ChromeCustomTabsInternalClient());
32+
this(new BrowserSwitchInspector(), new AuthTabInternalClient());
3033
}
3134

3235
@VisibleForTesting
3336
BrowserSwitchClient(BrowserSwitchInspector browserSwitchInspector,
34-
ChromeCustomTabsInternalClient customTabsInternalClient) {
37+
AuthTabInternalClient authTabInternalClient) {
3538
this.browserSwitchInspector = browserSwitchInspector;
36-
this.customTabsInternalClient = customTabsInternalClient;
39+
this.authTabInternalClient = authTabInternalClient;
40+
}
41+
42+
/**
43+
* Initialize the Auth Tab launcher. This should be called in the activity's onCreate()
44+
* before the activity is started.
45+
*/
46+
public void initializeAuthTabLauncher(@NonNull ComponentActivity activity,
47+
@NonNull AuthTabCallback callback) {
48+
this.authTabLauncher = AuthTabIntent.registerActivityResultLauncher(
49+
activity,
50+
result -> {
51+
BrowserSwitchFinalResult finalResult;
52+
53+
switch (result.resultCode) {
54+
case AuthTabIntent.RESULT_OK:
55+
if (result.resultUri != null && pendingAuthTabRequest != null) {
56+
finalResult = new BrowserSwitchFinalResult.Success(
57+
result.resultUri,
58+
pendingAuthTabRequest
59+
);
60+
} else {
61+
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
62+
}
63+
break;
64+
case AuthTabIntent.RESULT_CANCELED:
65+
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
66+
break;
67+
case AuthTabIntent.RESULT_VERIFICATION_FAILED:
68+
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
69+
break;
70+
case AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT:
71+
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
72+
break;
73+
default:
74+
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
75+
}
76+
callback.onResult(finalResult);
77+
pendingAuthTabRequest = null;
78+
}
79+
);
3780
}
3881

3982
/**
40-
* Open a browser or <a href="https://developer.chrome.com/multidevice/android/customtabs">Chrome Custom Tab</a>
41-
* with a given set of {@link BrowserSwitchOptions} from an Android activity.
83+
* Open a browser or Auth Tab with a given set of {@link BrowserSwitchOptions} from an Android activity.
4284
*
4385
* @param activity the activity used to start browser switch
4486
* @param browserSwitchOptions {@link BrowserSwitchOptions} the options used to configure the browser switch
4587
* @return a {@link BrowserSwitchStartResult.Started} that should be stored and passed to
46-
* {@link BrowserSwitchClient#completeRequest(Intent, String)} upon return to the app,
88+
* {@link BrowserSwitchClient#completeRequest(Intent, String)} upon return to the app (for Custom Tabs fallback),
4789
* or {@link BrowserSwitchStartResult.Failure} if browser could not be launched.
4890
*/
4991
@NonNull
50-
public BrowserSwitchStartResult start(@NonNull ComponentActivity activity, @NonNull BrowserSwitchOptions browserSwitchOptions) {
92+
public BrowserSwitchStartResult start(@NonNull ComponentActivity activity,
93+
@NonNull BrowserSwitchOptions browserSwitchOptions) {
5194
try {
5295
assertCanPerformBrowserSwitch(activity, browserSwitchOptions);
5396
} catch (BrowserSwitchException e) {
@@ -58,29 +101,53 @@ public BrowserSwitchStartResult start(@NonNull ComponentActivity activity, @NonN
58101
int requestCode = browserSwitchOptions.getRequestCode();
59102
String returnUrlScheme = browserSwitchOptions.getReturnUrlScheme();
60103
Uri appLinkUri = browserSwitchOptions.getAppLinkUri();
61-
62104
JSONObject metadata = browserSwitchOptions.getMetadata();
63105

64106
if (activity.isFinishing()) {
65107
String activityFinishingMessage =
66108
"Unable to start browser switch while host Activity is finishing.";
67109
return new BrowserSwitchStartResult.Failure(new BrowserSwitchException(activityFinishingMessage));
68-
} else {
69-
LaunchType launchType = browserSwitchOptions.getLaunchType();
70-
BrowserSwitchRequest request;
71-
try {
72-
request = new BrowserSwitchRequest(
73-
requestCode,
74-
browserSwitchUrl,
75-
metadata,
76-
returnUrlScheme,
77-
appLinkUri
78-
);
79-
customTabsInternalClient.launchUrl(activity, browserSwitchUrl, launchType);
80-
return new BrowserSwitchStartResult.Started(request.toBase64EncodedJSON());
81-
} catch (ActivityNotFoundException | BrowserSwitchException e) {
82-
return new BrowserSwitchStartResult.Failure(new BrowserSwitchException("Unable to start browser switch without a web browser.", e));
110+
}
111+
112+
LaunchType launchType = browserSwitchOptions.getLaunchType();
113+
BrowserSwitchRequest request;
114+
115+
try {
116+
request = new BrowserSwitchRequest(
117+
requestCode,
118+
browserSwitchUrl,
119+
metadata,
120+
returnUrlScheme,
121+
appLinkUri
122+
);
123+
124+
boolean useAuthTab = authTabInternalClient.isAuthTabSupported(activity);
125+
126+
if (useAuthTab) {
127+
this.pendingAuthTabRequest = request;
83128
}
129+
130+
authTabInternalClient.launchUrl(
131+
activity,
132+
browserSwitchUrl,
133+
returnUrlScheme,
134+
appLinkUri,
135+
authTabLauncher,
136+
launchType
137+
);
138+
139+
return new BrowserSwitchStartResult.Started(request.toBase64EncodedJSON());
140+
141+
} catch (ActivityNotFoundException e) {
142+
this.pendingAuthTabRequest = null;
143+
return new BrowserSwitchStartResult.Failure(
144+
new BrowserSwitchException("Unable to start browser switch without a web browser.", e)
145+
);
146+
} catch (Exception e) {
147+
this.pendingAuthTabRequest = null;
148+
return new BrowserSwitchStartResult.Failure(
149+
new BrowserSwitchException("Unable to start browser switch: " + e.getMessage(), e)
150+
);
84151
}
85152
}
86153

@@ -121,20 +188,18 @@ private boolean isValidRequestCode(int requestCode) {
121188
}
122189

123190
/**
124-
* Completes the browser switch flow and returns a browser switch result if a match is found for
125-
* the given {@link BrowserSwitchRequest}
191+
* Completes the browser switch flow for Custom Tabs fallback scenarios.
192+
* This method is still needed for devices that don't support Auth Tab.
193+
*
194+
* <p>See <a href="https://developer.chrome.com/docs/android/custom-tabs/guide-auth-tab#fallback_to_custom_tabs">
195+
* Auth Tab Fallback Documentation</a> for details on when Custom Tabs fallback is required
126196
*
127-
* @param intent the intent to return to your application containing a deep link result from the
128-
* browser flow
129-
* @param pendingRequest the pending request string returned from {@link BrowserSwitchStartResult.Started} via
130-
* {@link BrowserSwitchClient#start(ComponentActivity, BrowserSwitchOptions)}
131-
* @return a {@link BrowserSwitchFinalResult.Success} if the browser switch was successfully
132-
* completed, or {@link BrowserSwitchFinalResult.NoResult} if no result can be found for the given
133-
* pending request String. A {@link BrowserSwitchFinalResult.NoResult} will be
134-
* returned if the user returns to the app without completing the browser switch flow.
197+
* @param intent the intent to return to your application containing a deep link result
198+
* @param pendingRequest the pending request string returned from {@link BrowserSwitchStartResult.Started}
199+
* @return a {@link BrowserSwitchFinalResult}
135200
*/
136201
public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull String pendingRequest) {
137-
if (intent != null && intent.getData() != null) {
202+
if (intent.getData() != null) {
138203
Uri returnUrl = intent.getData();
139204

140205
try {
@@ -149,4 +214,8 @@ public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull
149214
}
150215
return BrowserSwitchFinalResult.NoResult.INSTANCE;
151216
}
152-
}
217+
218+
public boolean isAuthTabSupported(Context context) {
219+
return authTabInternalClient.isAuthTabSupported(context);
220+
}
221+
}

browser-switch/src/main/java/com/braintreepayments/api/ChromeCustomTabsInternalClient.java

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)