Skip to content

Commit 6ddac0d

Browse files
noguiersaperi22
andauthored
Add support for parametrized constructor
* 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 * Add support for parametrized constructor to give a merchant a choice to use AuthTab or not * -added the ability to store a callback internally without exposing it -modified BrowserSwitchClient's constructor to only accept Activity as a parameter -rerouted the flow to always call completeRequest and check for the callback result withing the function -added a call to completeRequest in DemoActivitySingleTop.java onResume -added the tests to support the new flow * -addressed PR suggestions -removed toast pop-ups * updated javadoc for parametrized BrowserSwitchClient constructor and completeRequest() function --------- Co-authored-by: saperi <[email protected]>
1 parent cc39a8a commit 6ddac0d

File tree

7 files changed

+235
-112
lines changed

7 files changed

+235
-112
lines changed

browser-switch/src/main/java/com/braintreepayments/api/AuthTabInternalClient.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ internal class AuthTabInternalClient (
3131
url: Uri,
3232
returnUrlScheme: String?,
3333
appLinkUri: Uri?,
34-
launcher: ActivityResultLauncher<Intent>,
34+
launcher: ActivityResultLauncher<Intent>?,
3535
launchType: LaunchType?
3636
) {
3737
val useAuthTab = isAuthTabSupported(context)
3838

39-
if (useAuthTab) {
39+
if (useAuthTab && launcher != null) {
4040
val authTabIntent = authTabIntentBuilder.build()
4141

4242
if (launchType == LaunchType.ACTIVITY_CLEAR_TOP) {

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

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.braintreepayments.api;
22

3+
import android.app.Activity;
34
import android.content.ActivityNotFoundException;
45
import android.content.Context;
56
import android.content.Intent;
67
import android.net.Uri;
78

89
import androidx.activity.ComponentActivity;
10+
import androidx.activity.result.ActivityResultCaller;
911
import androidx.activity.result.ActivityResultLauncher;
1012
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
1114
import androidx.annotation.VisibleForTesting;
1215
import androidx.browser.auth.AuthTabIntent;
1316

@@ -25,31 +28,78 @@ public class BrowserSwitchClient {
2528
private ActivityResultLauncher<Intent> authTabLauncher;
2629
private BrowserSwitchRequest pendingAuthTabRequest;
2730

31+
@Nullable
32+
private BrowserSwitchFinalResult authTabCallbackResult;
33+
2834
/**
29-
* Construct a client that manages the logic for browser switching.
35+
* Construct a client that manages browser switching with Chrome Custom Tabs fallback only.
36+
* This constructor does not initialize Auth Tab support. For Auth Tab functionality,
37+
* use {@link #BrowserSwitchClient(Activity)} instead.
3038
*/
3139
public BrowserSwitchClient() {
3240
this(new BrowserSwitchInspector(), new AuthTabInternalClient());
3341
}
3442

43+
/**
44+
* Construct a client that manages the logic for browser switching and automatically
45+
* initializes the Auth Tab launcher.
46+
*
47+
* <p>IMPORTANT: This constructor enables the AuthTab functionality, which has several caveats:
48+
*
49+
* <ul>
50+
* <li>The provided activity MUST implement {@link ActivityResultCaller}, which is true for all
51+
* instances of {@link androidx.activity.ComponentActivity}.
52+
* <li>{@link LaunchType#ACTIVITY_NEW_TASK} is not supported when using AuthTab and will be ignored.
53+
* Only {@link LaunchType#ACTIVITY_CLEAR_TOP} is supported with AuthTab.
54+
* <li>When using SingleTop activities, you must check for launcher results in {@code onResume()} as well
55+
* as in {@code onNewIntent()}, since the AuthTab activity result might be delivered during the
56+
* resuming phase.
57+
* <li>Care must be taken to avoid calling {@link #completeRequest(Intent, String)} multiple times
58+
* for the same result. Merchants should properly track their pending request state to ensure
59+
* the completeRequest method is only called once per browser switch session.
60+
* <li>AuthTab support is <strong>browser version dependent</strong>. It requires Chrome version 137
61+
* or higher on the user's device. On devices with older browser versions, the library will
62+
* automatically fall back to Custom Tabs. This means that enabling AuthTab is not guaranteed
63+
* to use the AuthTab flow if the user's browser version is too old.
64+
* </ul>
65+
*
66+
* <p>Consider using the default constructor {@link #BrowserSwitchClient()} if these limitations
67+
* are incompatible with your implementation.
68+
*
69+
* @param activity The activity used to initialize the Auth Tab launcher. Must implement
70+
* {@link ActivityResultCaller}.
71+
*/
72+
public BrowserSwitchClient(@NonNull Activity activity) {
73+
this(new BrowserSwitchInspector(), new AuthTabInternalClient());
74+
initializeAuthTabLauncher(activity);
75+
}
76+
3577
@VisibleForTesting
3678
BrowserSwitchClient(BrowserSwitchInspector browserSwitchInspector,
3779
AuthTabInternalClient authTabInternalClient) {
3880
this.browserSwitchInspector = browserSwitchInspector;
3981
this.authTabInternalClient = authTabInternalClient;
82+
this.authTabCallbackResult = null;
4083
}
4184

4285
/**
4386
* Initialize the Auth Tab launcher. This should be called in the activity's onCreate()
4487
* before the activity is started.
88+
*
89+
* @param activity The activity used to initialize the Auth Tab launcher
4590
*/
46-
public void initializeAuthTabLauncher(@NonNull ComponentActivity activity,
47-
@NonNull AuthTabCallback callback) {
91+
public void initializeAuthTabLauncher(@NonNull Activity activity) {
92+
93+
if (!(activity instanceof ActivityResultCaller)) {
94+
return;
95+
}
96+
97+
ComponentActivity componentActivity = (ComponentActivity) activity;
98+
4899
this.authTabLauncher = AuthTabIntent.registerActivityResultLauncher(
49-
activity,
100+
componentActivity,
50101
result -> {
51102
BrowserSwitchFinalResult finalResult;
52-
53103
switch (result.resultCode) {
54104
case AuthTabIntent.RESULT_OK:
55105
if (result.resultUri != null && pendingAuthTabRequest != null) {
@@ -61,19 +111,10 @@ public void initializeAuthTabLauncher(@NonNull ComponentActivity activity,
61111
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
62112
}
63113
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;
73114
default:
74115
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
75116
}
76-
callback.onResult(finalResult);
117+
this.authTabCallbackResult = finalResult;
77118
pendingAuthTabRequest = null;
78119
}
79120
);
@@ -91,6 +132,9 @@ public void initializeAuthTabLauncher(@NonNull ComponentActivity activity,
91132
@NonNull
92133
public BrowserSwitchStartResult start(@NonNull ComponentActivity activity,
93134
@NonNull BrowserSwitchOptions browserSwitchOptions) {
135+
136+
this.authTabCallbackResult = null;
137+
94138
try {
95139
assertCanPerformBrowserSwitch(activity, browserSwitchOptions);
96140
} catch (BrowserSwitchException e) {
@@ -121,7 +165,7 @@ public BrowserSwitchStartResult start(@NonNull ComponentActivity activity,
121165
appLinkUri
122166
);
123167

124-
boolean useAuthTab = authTabInternalClient.isAuthTabSupported(activity);
168+
boolean useAuthTab = isAuthTabSupported(activity);
125169

126170
if (useAuthTab) {
127171
this.pendingAuthTabRequest = request;
@@ -188,18 +232,27 @@ private boolean isValidRequestCode(int requestCode) {
188232
}
189233

190234
/**
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.
235+
* Completes the browser switch flow for both Auth Tab and Custom Tabs fallback scenarios.
236+
* This method first checks if we have a result from the Auth Tab callback,
237+
* and returns it if available. Otherwise, it follows the Custom Tabs flow.
193238
*
194239
* <p>See <a href="https://developer.chrome.com/docs/android/custom-tabs/guide-auth-tab#fallback_to_custom_tabs">
195240
* Auth Tab Fallback Documentation</a> for details on when Custom Tabs fallback is required
196241
*
242+
* <p><strong>IMPORTANT:</strong> When using Auth Tab with SingleTop activities, you must call this method
243+
* in both {@code onNewIntent()} <em>and</em> {@code onResume()} to ensure the result is properly processed
244+
* regardless of which launch mode is used.
245+
*
197246
* @param intent the intent to return to your application containing a deep link result
198247
* @param pendingRequest the pending request string returned from {@link BrowserSwitchStartResult.Started}
199248
* @return a {@link BrowserSwitchFinalResult}
200249
*/
201250
public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull String pendingRequest) {
202-
if (intent.getData() != null) {
251+
if (authTabCallbackResult != null) {
252+
BrowserSwitchFinalResult result = authTabCallbackResult;
253+
authTabCallbackResult = null;
254+
return result;
255+
} else if (intent.getData() != null) {
203256
Uri returnUrl = intent.getData();
204257

205258
try {
@@ -215,7 +268,14 @@ public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull
215268
return BrowserSwitchFinalResult.NoResult.INSTANCE;
216269
}
217270

218-
public boolean isAuthTabSupported(Context context) {
219-
return authTabInternalClient.isAuthTabSupported(context);
271+
/**
272+
* Checks if Auth Tab is supported on this device and if the launcher has been initialized.
273+
* @param context The application context
274+
* @return true if Auth Tab is supported by the browser AND the launcher has been initialized,
275+
* false otherwise
276+
*/
277+
@VisibleForTesting
278+
boolean isAuthTabSupported(Context context) {
279+
return authTabLauncher != null && authTabInternalClient.isAuthTabSupported(context);
220280
}
221281
}

browser-switch/src/test/java/com/braintreepayments/api/AuthTabInternalClientUnitTest.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ class AuthTabInternalClientUnitTest {
157157
}
158158
}
159159

160-
161160
@Test
162161
fun `launchUrl handles app link with no path`() {
163162
val appLinkUri = Uri.parse("https://example.com")
@@ -216,4 +215,20 @@ class AuthTabInternalClientUnitTest {
216215

217216
assertTrue(intent.flags and Intent.FLAG_ACTIVITY_CLEAR_TOP != 0)
218217
}
219-
}
218+
219+
@Test
220+
fun `launchUrl with null launcher falls back to Custom Tabs even when Auth Tab is supported`() {
221+
val packageName = "com.android.chrome"
222+
every { CustomTabsClient.getPackageName(context, null) } returns packageName
223+
every { CustomTabsClient.isAuthTabSupported(context, packageName) } returns true
224+
225+
val client = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
226+
val returnUrlScheme = "example"
227+
228+
client.launchUrl(context, url, returnUrlScheme, null, null, null)
229+
230+
verify {
231+
customTabsIntent.launchUrl(context, url)
232+
}
233+
}
234+
}

0 commit comments

Comments
 (0)