Skip to content
Merged
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
Expand Up @@ -55,6 +55,7 @@ internal class AuthTabInternalClient(
launchCustomTabs(context, url, launchType)
}
}

private fun launchCustomTabs(context: Context, url: Uri, launchType: LaunchType?) {
val customTabsIntent = customTabsIntentBuilder.build()
when (launchType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import android.content.Intent;
import android.net.Uri;

import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.ActivityResultRegistry;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
Expand All @@ -27,9 +29,41 @@ public class BrowserSwitchClient {
private ActivityResultLauncher<Intent> authTabLauncher;
private BrowserSwitchRequest pendingAuthTabRequest;

final String registryKey = "BrowserSwitchActivityRegistryKey";
Comment thread
tdchow marked this conversation as resolved.

@Nullable
private BrowserSwitchFinalResult authTabCallbackResult;

@Nullable
private AuthTabIntent.AuthResult authTabResult;

ActivityResultCallback<AuthTabIntent.AuthResult> authTabCallback = new ActivityResultCallback<>() {
@Override
public void onActivityResult(AuthTabIntent.AuthResult result) {
authTabResult = result;
}
};

void onAuthTabResult(AuthTabIntent.AuthResult 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;
default:
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
}
authTabCallbackResult = finalResult;
pendingAuthTabRequest = null;
}

/**
* Construct a client that manages browser switching with Chrome Custom Tabs fallback only.
* This constructor does not initialize Auth Tab support. For Auth Tab functionality,
Expand Down Expand Up @@ -73,6 +107,38 @@ public BrowserSwitchClient(@NonNull ActivityResultCaller caller) {
initializeAuthTabLauncher(caller);
}

/**
* Construct a client that manages the logic for browser switching and automatically
* initializes the Auth Tab launcher. Use this constructor for flows where {@link ActivityResultCaller} is not
* available.
*
* <p>IMPORTANT: This constructor enables the AuthTab functionality, which has several caveats:
*
* <ul>
* <li>{@link LaunchType#ACTIVITY_NEW_TASK} is not supported when using AuthTab and will be ignored.
* Only {@link LaunchType#ACTIVITY_CLEAR_TOP} is supported with AuthTab.
* <li>When using SingleTop activities, you must check for launcher results in {@code onResume()} as well
* as in {@code onNewIntent()}, since the AuthTab activity result might be delivered during the
* resuming phase.
* <li>Care must be taken to avoid calling {@link #completeRequest(Intent, String)} multiple times
* for the same result. Merchants should properly track their pending request state to ensure
* the completeRequest method is only called once per browser switch session.
* <li>AuthTab support is <strong>browser version dependent</strong>. It requires Chrome version 137
* or higher on the user's device. On devices with older browser versions, the library will
* automatically fall back to Custom Tabs. This means that enabling AuthTab is not guaranteed
* to use the AuthTab flow if the user's browser version is too old.
* </ul>
*
* <p>Consider using the default constructor {@link #BrowserSwitchClient()} if these limitations
* are incompatible with your implementation.
*
* @param registry The ActivityResultRegistry used to initialize the Auth Tab launcher
*/
public BrowserSwitchClient(@NonNull ActivityResultRegistry registry) {
this(new BrowserSwitchInspector(), new AuthTabInternalClient());
initializeAuthTabLauncher(registry);
}

@VisibleForTesting
BrowserSwitchClient(BrowserSwitchInspector browserSwitchInspector,
AuthTabInternalClient authTabInternalClient) {
Expand All @@ -88,38 +154,34 @@ public BrowserSwitchClient(@NonNull ActivityResultCaller caller) {
initializeAuthTabLauncher(caller);
}

@VisibleForTesting
BrowserSwitchClient(@NonNull ActivityResultRegistry registry,
BrowserSwitchInspector browserSwitchInspector,
AuthTabInternalClient authTabInternalClient) {
this(browserSwitchInspector, authTabInternalClient);
initializeAuthTabLauncher(registry);
}

/**
* Initialize the Auth Tab launcher. This should be called in the activity/fragment's onCreate()
* before it is started.
*
* @param caller The ActivityResultCaller (Activity or Fragment) used to initialize the Auth Tab launcher
*/
private void initializeAuthTabLauncher(@NonNull ActivityResultCaller caller) {

this.authTabLauncher = AuthTabIntent.registerActivityResultLauncher(
private void initializeAuthTabLauncher(@NonNull ActivityResultCaller caller) {
authTabLauncher = AuthTabIntent.registerActivityResultLauncher(
caller,
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;
default:
finalResult = BrowserSwitchFinalResult.NoResult.INSTANCE;
}
this.authTabCallbackResult = finalResult;
pendingAuthTabRequest = null;
}
authTabCallback
);
}

private void initializeAuthTabLauncher(@NonNull ActivityResultRegistry registry) {
authTabLauncher = registry.register(
registryKey,
new AuthTabIntent.AuthenticateUserResultContract(),
authTabCallback
);
}

/**
* Restores a pending request after process kill or app restart.
Expand Down Expand Up @@ -150,6 +212,7 @@ public BrowserSwitchStartResult start(@NonNull Activity activity,
@NonNull BrowserSwitchOptions browserSwitchOptions) {
return start(activity, browserSwitchOptions, false);
}

/**
* Open a browser or Auth Tab with a given set of {@link BrowserSwitchOptions} from an Android activity.
*
Expand Down Expand Up @@ -286,6 +349,10 @@ private boolean isValidRequestCode(int requestCode) {
* @return a {@link BrowserSwitchFinalResult}
*/
public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull String pendingRequest) {
if (authTabResult != null) {
onAuthTabResult(authTabResult);
authTabResult = null;
}
if (authTabCallbackResult != null) {
BrowserSwitchFinalResult result = authTabCallbackResult;
authTabCallbackResult = null;
Expand Down Expand Up @@ -316,4 +383,4 @@ public BrowserSwitchFinalResult completeRequest(@NonNull Intent intent, @NonNull
boolean isAuthTabSupported(Context context) {
return authTabLauncher != null && authTabInternalClient.isAuthTabSupported(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.ActivityResultRegistry;
import androidx.browser.auth.AuthTabIntent;

import org.json.JSONException;
Expand Down Expand Up @@ -283,6 +284,18 @@ public void initializeAuthTabLauncher_registersLauncherWithActivity() {
}
}

@Test
public void initializeAuthTabLauncher_withActivityResultRegistry_callsRegister() {
ActivityResultRegistry registry = mock(ActivityResultRegistry.class);
BrowserSwitchClient sut = new BrowserSwitchClient(registry, browserSwitchInspector, authTabInternalClient);

verify(registry).register(
eq(sut.registryKey),
any(AuthTabIntent.AuthenticateUserResultContract.class),
eq(sut.authTabCallback)
);
}

@Test
public void start_withAuthTabLauncherInitialized_usesPendingAuthTabRequest() throws BrowserSwitchException {
try (MockedStatic<AuthTabIntent> mockedAuthTab = mockStatic(AuthTabIntent.class)) {
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ buildscript {
ext.deps = [
'annotation' : 'androidx.annotation:annotation:1.7.0',
'appcompat' : 'androidx.appcompat:appcompat:1.6.0',
'browser' : 'androidx.browser:browser:1.9.0',
'browser' : 'androidx.browser:browser:1.10.0-alpha02',
Comment thread
tdchow marked this conversation as resolved.
'kotlin' : 'org.jetbrains.kotlin:kotlin-stdlib:1.9.20',

// test dependencies
Expand All @@ -38,7 +38,7 @@ buildscript {
plugins {
id 'io.github.gradle-nexus.publish-plugin' version '1.1.0'
id 'org.jetbrains.dokka' version '1.9.10'
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
id 'io.gitlab.arturbosch.detekt' version '1.23.6'
}

Expand Down
4 changes: 3 additions & 1 deletion demo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ android {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.4.3'
kotlinCompilerExtensionVersion '1.5.3'
}
packagingOptions {
resources {
Expand All @@ -58,6 +58,8 @@ dependencies {
implementation platform('androidx.compose:compose-bom:2023.03.00')
implementation 'androidx.compose.material3:material3'
implementation 'androidx.core:core-ktx:1.13.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.10.0'

androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
Expand Down
Loading