Skip to content

Conversation

@eri9000
Copy link
Collaborator

@eri9000 eri9000 commented Dec 6, 2025

Related to issue: https://devtopia.esri.com/runtime/kotlin/issues/6659

Summary of changes:

  • Adds test to verify AuthenticationActivity behavior
  • Modifies AuthenticationActivity to process incoming redirects in through onCreate.
  • Provides better error handling when
  • Modifies OAuthUserSignInContract to handle errors coming back in the Result
  • Creates a new Authenticator Exceptions class

Pre-merge Checklist

  • a vTest Job for this PR has been run
    • link:
  • Unit and/or integration tests have been added to exercise this PR's logic, and the tests are passing:
    • Yes
    • No

@eri9000 eri9000 changed the base branch from main to v.next December 6, 2025 00:13
@eri9000 eri9000 changed the title Erick/fail when custom tabs not found Fail gracefully when the default browser does not support custom tabs Dec 6, 2025
@eri9000 eri9000 mentioned this pull request Dec 8, 2025
3 tasks
@eri9000 eri9000 requested a review from gunt0001 December 8, 2025 22:16
@eri9000 eri9000 marked this pull request as ready for review December 8, 2025 22:16
/**
* Given [AuthenticationActivity] with a browser that supports Custom Tabs
* When [AuthenticationActivity] starts
* Then an intent should be fired with ACTION_VIEW, simulating a Custom Tabs launch
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm confused - looking at processAuthenticationFlow(), when an intent is handled with ACTION_VIEW, that means we are getting the redirect from Custom Tabs. It doesn't mean we are launching Custom Tabs. If this test is meant to check that the custom tabsl intent is being launched, shouldn't we be checking for something like the intent being of type CustomTabsIntent?

Copy link
Collaborator Author

@eri9000 eri9000 Dec 12, 2025

Choose a reason for hiding this comment

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

These are the following intents that are captured by the test,

AuthenticationActivityTest: Intent: action=null data=null extras=KEY_INTENT_EXTRA_URL
AuthenticationActivityTest: Intent: action=android.intent.action.VIEW data=https://example.com/auth extras=android.support.customtabs.extra.SESSION, android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS, androidx.browser.customtabs.extra.SHARE_STATE, com.android.browser.headers

As you can see, the first one is the one that the test uses to launch AuthenticationActivity, simulating the OAuthUserSignInContract.

And the second one is the CustomTabs intent that AuthenticationActivity launches.
Since we are not signing in, we do not see the "redirect" intent that we would get from the browser

If this test is meant to check that the custom tabs intent is being launched, shouldn't we be checking for something like the intent being of type CustomTabsIntent?

I assume you mean some identifiable information that it will tell us that it is the CustomTabs Intent, I already check for android.support.customtabs.extra.SESSION inside the test but I could add more if you think is necessary

scenario.close()
val result = scenario.result
assertThat(result.resultCode).isEqualTo(RESULT_CODE_SUCCESS)
assertThat(result.resultData?.getStringExtra("KEY_INTENT_EXTRA_RESPONSE_URI")).isEqualTo(redirectUri)
Copy link
Collaborator

Choose a reason for hiding this comment

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

How is this test working? In other words, what closes the Custom Tabs and redirects into AuthenticationActivity with RESULT_CODE_SUCCESS?

Copy link
Collaborator Author

@eri9000 eri9000 Dec 12, 2025

Choose a reason for hiding this comment

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

This test does not open CustomTabs, it only verifies that AuthenticationActivity handles a "redirect" URI successfully.
It does this by launching the activity with an intent that has a redirect_uri as data, simulating a browser redirecting back to the activity.

When AuthenticationActivity processes a redirect URI successfully, it calls setResult(RESULT_CODE_SUCCESS, newIntent) here

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, makes sense. Would the following doc make that clearer?

/**
     * Given [AuthenticationActivity] receives an intent containing a valid redirect URI
     * When the activity processes the redirect intent
     * Then the activity finishes with RESULT_CODE_SUCCESS and includes the redirect URI in the result data
     * @since 300.0.0
     */

* an appropriate error message.
* @since 300.0.0
*/
private fun processAuthenticationFlow() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick - naming suggestion:

Suggested change
private fun processAuthenticationFlow() {
private fun promptWithCustomTabs() {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i'd like to give it a more generic name since this likely will be the method to launch external browser or Auth Tabs (when we support it).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought we won't support external browser? And when we move to Auth Tabs, we can rename the function to promptWithAuthTab at that point.

val authEndpoint = getStringExtra(KEY_INTENT_EXTRA_URL)
when {
// This signals that we have been redirected back to the app from the browser
action == Intent.ACTION_VIEW && data != null -> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this case only happening when we get an intent in onNewIntent? It seems we ar enow conflating tow different cases in processAuthenticationFlow:

  1. launching custom tabs
  2. handling the redirect from custom tabs

I think this function here should only handle the first case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When testing different browsers sign-in, i found that some browsers might redirect with different flags and might come through onCreate instead of onNewIntent.
This is just safe guarding this possible outcome.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is diverging quite a bit from the current implementation. At the minimum we will need test cases for the scenarios you mention. Lets discuss...

@eri9000 eri9000 requested a review from gunt0001 December 12, 2025 23:38
@eri9000
Copy link
Collaborator Author

eri9000 commented Dec 12, 2025

@gunt0001 Please have another look

* @since 300.0.0
*/
@RunWith(AndroidJUnit4::class)
class AuthenticationActivityExternalBrowserTest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we name it simply?

Suggested change
class AuthenticationActivityExternalBrowserTest {
class AuthenticationActivityTest {

Comment on lines +92 to +97
/**
* Given [AuthenticationActivity] is launched with a default browser that does not support Custom Tabs
* When [AuthenticationActivity] starts
* Then the activity finishes with RESULT_CODE_CANCELED and includes an exception message in the result data
* @since 300.0.0
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would the following be a better description for this test?

Suggested change
/**
* Given [AuthenticationActivity] is launched with a default browser that does not support Custom Tabs
* When [AuthenticationActivity] starts
* Then the activity finishes with RESULT_CODE_CANCELED and includes an exception message in the result data
* @since 300.0.0
*/
/**
* Given a device with default browser that does not support Custom Tabs
* When [AuthenticationActivity] receives an intent for OAuth sign-in
* Then the activity finishes with RESULT_CODE_CANCELED and includes an exception message in the result data
* @since 300.0.0
*/


// Launch the AuthenticationActivity with the redirect intent
val scenario = ActivityScenario.launchActivityForResult<AuthenticationActivity>(intent)
scenario.close()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looking at the doc of close, should we be using scenario.use { ... } here or alternatively ActivityScenarioRule? The same would apply to the other tests.

scenario.close()
val result = scenario.result
assertThat(result.resultCode).isEqualTo(RESULT_CODE_SUCCESS)
assertThat(result.resultData?.getStringExtra("KEY_INTENT_EXTRA_RESPONSE_URI")).isEqualTo(redirectUri)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, makes sense. Would the following doc make that clearer?

/**
     * Given [AuthenticationActivity] receives an intent containing a valid redirect URI
     * When the activity processes the redirect intent
     * Then the activity finishes with RESULT_CODE_SUCCESS and includes the redirect URI in the result data
     * @since 300.0.0
     */

* @since 300.0.0
*/
@Test
fun returnsExceptionWhenNoBrowserAvailable() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be named?

Suggested change
fun returnsExceptionWhenNoBrowserAvailable() {
fun returnsExceptionWhenNoCustomTabsAvailable() {

* an appropriate error message.
* @since 300.0.0
*/
private fun processAuthenticationFlow() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought we won't support external browser? And when we move to Auth Tabs, we can rename the function to promptWithAuthTab at that point.

val authEndpoint = getStringExtra(KEY_INTENT_EXTRA_URL)
when {
// This signals that we have been redirected back to the app from the browser
action == Intent.ACTION_VIEW && data != null -> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is diverging quite a bit from the current implementation. At the minimum we will need test cases for the scenarios you mention. Lets discuss...

@eri9000
Copy link
Collaborator Author

eri9000 commented Dec 16, 2025

a simpler implementation is in PR #1044 since we want to make gradual changes

@eri9000 eri9000 closed this Dec 16, 2025
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.

3 participants