Skip to content

feat: Added Auth tab support#962

Open
pmathew92 wants to merge 11 commits intov4_developmentfrom
auth_tab_support
Open

feat: Added Auth tab support#962
pmathew92 wants to merge 11 commits intov4_developmentfrom
auth_tab_support

Conversation

@pmathew92
Copy link
Copy Markdown
Contributor

Changes

This PT adds support for Android Auth TabIntent to the WebAuthentication login and logout flow.

References

https://developer.android.com/reference/androidx/browser/auth/AuthTabIntent

Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. Since this library has unit testing, tests should be added for new functionality and existing tests should complete without errors.

  • This change adds unit test coverage

  • This change adds integration test coverage

  • This change has been tested on the latest version of the platform/language or why not

Checklist

@pmathew92 pmathew92 marked this pull request as ready for review April 30, 2026 06:58
@pmathew92 pmathew92 requested a review from a team as a code owner April 30, 2026 06:58
@Suppress("DEPRECATION")
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// If the Activity Result API (e.g. auth tab) already handled this result and called
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove this generated comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is added intentionally to highlight why this if check is needed


override fun onResume() {
super.onResume()
// Auth Tab results are delivered via the Activity Result API callback (onAuthTabResult)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove this generated comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is added intentionally to highlight why this if check is needed

customTabsClientMock = Mockito.mockStatic(CustomTabsClient.class);
customTabsClientMock.when(() -> CustomTabsClient.isAuthTabSupported(eq(context), any(String.class))).thenReturn(true);

CustomTabsOptions ctOptions = CustomTabsOptions.newBuilder()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

missing .withAuthTab() — test never enters the auth tab code path, passes for the wrong reason

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the test now has .withAuthTab() but still uses the default BrowserPicker (no mock returning a package). This means preferredPackage will be null, so the method falls back at the first guard (preferredPackage == null) — not at the scheme check it's supposed to test. The test still passes for the wrong reason, just at a different fallback point.

@ExperimentalAuth0Api
@NonNull
public Builder withEphemeralBrowsing() {
Builder withEphemeralBrowsing() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

visibility reduced from public to package-private without callout; this is a breaking API change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No.. this is inside the CustomTabsOptions class. Not the public API

import com.google.androidbrowserhelper.trusted.TwaLauncher

public open class AuthenticationActivity : Activity() {
public open class AuthenticationActivity : ComponentActivity() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ComponentActivity() — base class change from Activity is public API-breaking for subclassers;
Can we document in V4 migration guide

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is within the SDK. Not for public APIs. It is open for testing purpose. Do you see scenario where this can fail for users updating to V4 ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since this targets v4 and subclassing AuthenticationActivity isn't a documented use case, this is fine. No action needed.

}
private var customTabsController: CustomTabsController? = null
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

signature changed from nullable to non-null; subclass overrides of the old signature will break

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Again an internal implementation. No effect for consuming clients

null,
TwaLauncher.CCT_FALLBACK_STRATEGY
);
} else if (customTabsOptions.isAuthTab()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

else if (customTabsOptions.isAuthTab()) — TWA silently takes priority over Auth Tab; consider logging a warning or documenting this precedence

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added comments and document to highlight this

public open class AuthenticationActivity : Activity() {
public open class AuthenticationActivity : ComponentActivity() {
private var intentLaunched = false
internal val authTabResultHandler = AuthTabResultHandler(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

internal val authTabResultHandler — internal exposes to entire module, not just tests; consider private + @VisibleForTesting(PRIVATE)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We intentionally chose internal over private + @VisibleForTesting as part of the collaborator pattern

  • AuthTabResultHandler is internal itself, so the combination is module-scoped regardless
  • The collaborator pattern was specifically chosen to avoid @VisibleForTesting on the activity — adding it back would contradict the design
  • Tests in the same module access it cleanly without annotation

return;
}

bindService();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

bindService() called before scheme null-check at line 198; if scheme is null, this work is wasted before falling back to launchAsDefault which calls bindService again

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. Moved scheme check above bindService()

customTabsClientMock = Mockito.mockStatic(CustomTabsClient.class);
customTabsClientMock.when(() -> CustomTabsClient.isAuthTabSupported(eq(context), any(String.class))).thenReturn(true);

CustomTabsOptions ctOptions = CustomTabsOptions.newBuilder()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the test now has .withAuthTab() but still uses the default BrowserPicker (no mock returning a package). This means preferredPackage will be null, so the method falls back at the first guard (preferredPackage == null) — not at the scheme check it's supposed to test. The test still passes for the wrong reason, just at a different fallback point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants