Skip to content

feat: add Partial Custom Tabs support for Android#846

Open
utkrishtsahu wants to merge 5 commits into
mainfrom
feature/partial-custom-tabs
Open

feat: add Partial Custom Tabs support for Android#846
utkrishtsahu wants to merge 5 commits into
mainfrom
feature/partial-custom-tabs

Conversation

@utkrishtsahu
Copy link
Copy Markdown
Contributor

  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

📋 Changes

Adds support for Partial Custom Tabs in auth0-flutter, allowing the authentication page to be displayed as a bottom sheet or side sheet on Android instead of a full-screen browser tab.

New types:

  • CustomTabsOptions — Dart class mirroring the iOS SafariViewController pattern, with properties: initialHeight, resizable, toolbarCornerRadius, initialWidth, sideSheetBreakpoint, backgroundInteractionEnabled, allowedBrowsers

Modified methods:

  • WebAuthentication.login() — added optional customTabsOptions parameter
  • WebAuthentication.logout() — added optional customTabsOptions parameter

Deprecated:

  • Top-level allowedBrowsers parameter on login() and logout() — replaced by CustomTabsOptions.allowedBrowsers

Native (Kotlin):

  • LoginWebAuthRequestHandler and LogoutWebAuthRequestHandler parse the new customTabsOptions map and build CustomTabsOptions via the Auth0.Android SDK builder
  • Bumped com.auth0.android:auth0 from 3.12.0 to 3.16.0

Backward compatibility: Fully non-breaking. The new parameter defaults to null. The deprecated allowedBrowsers continues to work as a fallback when customTabsOptions is absent.

Usage:

await auth0.webAuthentication().login( customTabsOptions: const CustomTabsOptions( initialHeight: 700, toolbarCornerRadius: 16, backgroundInteractionEnabled: true, ), );

📎 References

SDK-8720

🎯 Testing

Unit tests added:

  • 5 Dart tests in web_authentication_test.dart — verifies customTabsOptions is serialized correctly for login/logout, defaults to null, and side sheet options pass through
  • 5 Kotlin tests in LoginWebAuthRequestHandlerTest.kt — verifies partial tabs parsing with initialHeight, all options, absence, priority over top-level allowedBrowsers, and fallback behavior
  • 5 Kotlin tests in LogoutWebAuthRequestHandlerTest.kt — same coverage as login handler

Not tested:

  • No iOS changes; customTabsOptions is Android-only and ignored on other platforms

(customTabsOptionsMap["allowedBrowsers"] as? List<*>)?.filterIsInstance<String>().orEmpty()
} else {
(args["allowedBrowsers"] as? List<*>)?.filterIsInstance<String>().orEmpty()
}
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.

Should we handle the edge scenario where user can send both customTabsOptions and allowedBrowsers ?

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.

Yes, this is already handled! When both are provided, customTabsOptions.allowedBrowsers wins and the top-level one is ignored. I've now extracted this into a shared utility with a KDoc comment that makes the precedence explicit. There's also a test specifically for this case (uses allowedBrowsers from customTabsOptions when both are provided).

}

if (customTabsOptionsMap != null) {
val initialHeight = customTabsOptionsMap["initialHeight"] as? Int
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.

Pbly extract all these checks to a Util method to be reused for both login and logout ?

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.

Created buildCustomTabsOptions() in the utils package. Both handlers now just do:

buildCustomTabsOptions(args)?.let { builder.withCustomTabsOptions(it) }
Much cleaner and keeps them in sync.

)

runRequestHandler(args) { _, builder ->
verify(builder).withCustomTabsOptions(any())
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.

This is validating any property being passed. Not the exact property as mentioned in the title

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.

Fair point. The issue is that CustomTabsOptions has all private fields with no public getters, so we can't directly assert on the built object from the handler level. I've added a dedicated CustomTabsOptionsBuilderTest that tests our utility function and uses reflection to verify the exact values (initialHeight=700, cornerRadius=16, etc.). The handler tests now just verify the routing — that withCustomTabsOptions gets called or not based on the input.

)

runRequestHandler(args) { _, builder ->
verify(builder).withCustomTabsOptions(any())
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.

Same as above

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.

Addressed — same approach as above. Value-level assertions live in CustomTabsOptionsBuilderTest.

)

runRequestHandler(args) { _, builder ->
verify(builder).withCustomTabsOptions(any())
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.

Validation is vague here ?

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.

Fixed in the new utility test. For example, the precedence test asserts that the packages list is exactly ["com.android.chrome"] (from customTabsOptions) and not ["org.mozilla.firefox"] (from top-level). Same for the fallback test — it asserts the exact list passed via top-level allowedBrowsers.

)

runRequestHandler(args) { _, builder ->
verify(builder).withCustomTabsOptions(any())
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.

Same as above

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.

Fixed in the new utility test. For example, the precedence test asserts that the packages list is exactly ["com.android.chrome"] (from customTabsOptions) and not ["org.mozilla.firefox"] (from top-level). Same for the fallback test — it asserts the exact list passed via top-level allowedBrowsers.

(customTabsOptionsMap["allowedBrowsers"] as? List<*>)?.filterIsInstance<String>().orEmpty()
} else {
(args["allowedBrowsers"] as? List<*>)?.filterIsInstance<String>().orEmpty()
}
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.

There is still a edge-case where if user passes customTabsOptionsMap !=null but doesn't contain allowed browser list and args["allowedBrowsers"] is passed with a valid list. In this scenario, we would completely skip extracting the allowedBrowsers. Not a hard requirement . Skip if you think this won't happen or chance are very low

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