diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/HostedUIClient.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/HostedUIClient.kt index 7f6e151605..d5f7699ba4 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/HostedUIClient.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/HostedUIClient.kt @@ -63,7 +63,8 @@ internal class HostedUIClient private constructor( launchCustomTabs( uri = createAuthorizeUri(hostedUIOptions), activity = hostedUIOptions.callingActivity, - customBrowserPackage = hostedUIOptions.browserPackage + customBrowserPackage = hostedUIOptions.browserPackage, + preferPrivateSession = hostedUIOptions.preferPrivateSession ) } @@ -75,14 +76,22 @@ internal class HostedUIClient private constructor( ) } - private fun launchCustomTabs(uri: Uri, activity: Activity? = null, customBrowserPackage: String?) { + private fun launchCustomTabs( + uri: Uri, + activity: Activity? = null, + customBrowserPackage: String?, + preferPrivateSession: Boolean? = null // allowing nullable, as null means customer didn't specify + ) { if (!BrowserHelper.isBrowserInstalled(context)) { throw RuntimeException("No browsers installed") } val browserPackage = customBrowserPackage ?: defaultCustomTabsPackage - val customTabsIntent = CustomTabsIntent.Builder(session).build().apply { + val customTabsIntent = CustomTabsIntent.Builder(session).apply { + // If customer didn't specify (null), don't add any Intent extra + preferPrivateSession?.let { setEphemeralBrowsingEnabled(it) } + }.build().apply { browserPackage?.let { intent.`package` = it } intent.data = uri } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelper.kt index dffaf2b8bb..3856aa5326 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelper.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelper.kt @@ -35,7 +35,8 @@ internal object HostedUIHelper { authProvider = authProvider, idpIdentifier = (options as? AWSCognitoAuthWebUISignInOptions)?.idpIdentifier ), - browserPackage = (options as? AWSCognitoAuthWebUISignInOptions)?.browserPackage + browserPackage = (options as? AWSCognitoAuthWebUISignInOptions)?.browserPackage, + preferPrivateSession = options.preferPrivateSession ) /** diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthWebUISignInOptions.java b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthWebUISignInOptions.java index d1e9fd4c9d..d44da58875 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthWebUISignInOptions.java +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthWebUISignInOptions.java @@ -37,13 +37,15 @@ public final class AWSCognitoAuthWebUISignInOptions extends AuthWebUISignInOptio * @param idpIdentifier The IdentityProvider identifier if using multiple instances of same identity provider. * @param browserPackage Specify which browser package should be used for web sign in (e.g. "org.mozilla.firefox"). * Defaults to the Chrome package if not specified. + * @param preferPrivateSession specifying whether or not to launch web ui in an ephemeral CustomTab. */ protected AWSCognitoAuthWebUISignInOptions( List scopes, String idpIdentifier, - String browserPackage + String browserPackage, + Boolean preferPrivateSession ) { - super(scopes); + super(scopes, preferPrivateSession); this.idpIdentifier = idpIdentifier; this.browserPackage = browserPackage; } @@ -80,7 +82,8 @@ public int hashCode() { return ObjectsCompat.hash( getScopes(), getIdpIdentifier(), - getBrowserPackage() + getBrowserPackage(), + getPreferPrivateSession() ); } @@ -94,7 +97,8 @@ public boolean equals(Object obj) { AWSCognitoAuthWebUISignInOptions webUISignInOptions = (AWSCognitoAuthWebUISignInOptions) obj; return ObjectsCompat.equals(getScopes(), webUISignInOptions.getScopes()) && ObjectsCompat.equals(getIdpIdentifier(), webUISignInOptions.getIdpIdentifier()) && - ObjectsCompat.equals(getBrowserPackage(), webUISignInOptions.getBrowserPackage()); + ObjectsCompat.equals(getBrowserPackage(), webUISignInOptions.getBrowserPackage()) && + ObjectsCompat.equals(getPreferPrivateSession(), webUISignInOptions.getPreferPrivateSession()); } } @@ -104,6 +108,7 @@ public String toString() { "scopes=" + getScopes() + ", idpIdentifier=" + getIdpIdentifier() + ", browserPackage=" + getBrowserPackage() + + ", preferPrivateSession=" + getPreferPrivateSession() + '}'; } @@ -162,7 +167,8 @@ public AWSCognitoAuthWebUISignInOptions build() { return new AWSCognitoAuthWebUISignInOptions( Immutable.of(super.getScopes()), idpIdentifier, - browserPackage + browserPackage, + super.getPreferPrivateSession() ); } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/HostedUIOptions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/HostedUIOptions.kt index 2f4d491e88..fc67328736 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/HostedUIOptions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/HostedUIOptions.kt @@ -21,5 +21,6 @@ internal data class HostedUIOptions( val callingActivity: Activity, val scopes: List?, val providerInfo: HostedUIProviderInfo, - val browserPackage: String? + val browserPackage: String?, + val preferPrivateSession: Boolean? ) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelperTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelperTest.kt new file mode 100644 index 0000000000..fe0e720b21 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelperTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.helpers + +import android.app.Activity +import com.amplifyframework.auth.AuthProvider +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.junit.Test + +class HostedUIHelperTest { + + private val mockActivity = mockk() + + @Test + fun `createHostedUIOptions with AWSCognitoAuthWebUISignInOptions and preferPrivateSession true`() { + val scopes = listOf("openid", "profile") + val options = AWSCognitoAuthWebUISignInOptions.builder() + .scopes(scopes) + .browserPackage("com.android.chrome") + .preferPrivateSession(true) + .build() + + val hostedUIOptions = HostedUIHelper.createHostedUIOptions( + callingActivity = mockActivity, + authProvider = AuthProvider.google(), + options = options + ) + + hostedUIOptions.callingActivity shouldBe mockActivity + hostedUIOptions.scopes shouldBe scopes + hostedUIOptions.browserPackage shouldBe "com.android.chrome" + hostedUIOptions.preferPrivateSession shouldBe true + } + + @Test + fun `createHostedUIOptions with AWSCognitoAuthWebUISignInOptions and preferPrivateSession false`() { + val scopes = listOf("openid", "email") + val options = AWSCognitoAuthWebUISignInOptions.builder() + .scopes(scopes) + .browserPackage("org.mozilla.firefox") + .preferPrivateSession(false) + .build() + + val hostedUIOptions = HostedUIHelper.createHostedUIOptions( + callingActivity = mockActivity, + authProvider = AuthProvider.facebook(), + options = options + ) + + hostedUIOptions.callingActivity shouldBe mockActivity + hostedUIOptions.scopes shouldBe scopes + hostedUIOptions.browserPackage shouldBe "org.mozilla.firefox" + hostedUIOptions.preferPrivateSession shouldBe false + } + + @Test + fun `createHostedUIOptions with AWSCognitoAuthWebUISignInOptions and preferPrivateSession null`() { + val scopes = listOf("openid") + val options = AWSCognitoAuthWebUISignInOptions.builder() + .scopes(scopes) + .browserPackage("com.android.chrome") + .build() + + val hostedUIOptions = HostedUIHelper.createHostedUIOptions( + callingActivity = mockActivity, + authProvider = null, + options = options + ) + + hostedUIOptions.callingActivity shouldBe mockActivity + hostedUIOptions.scopes shouldBe scopes + hostedUIOptions.browserPackage shouldBe "com.android.chrome" + hostedUIOptions.preferPrivateSession shouldBe null + } + + @Test + fun `selectRedirectUri prefers non-HTTP scheme`() { + val redirectUris = listOf( + "https://example.com/callback", + "myapp://auth/callback", + "http://localhost:3000/callback" + ) + + val selectedUri = HostedUIHelper.selectRedirectUri(redirectUris) + selectedUri shouldBe "myapp://auth/callback" + } + + @Test + fun `selectRedirectUri returns first URI when no non-HTTP scheme available`() { + val redirectUris = listOf( + "https://example.com/callback", + "http://localhost:3000/callback" + ) + + val selectedUri = HostedUIHelper.selectRedirectUri(redirectUris) + selectedUri shouldBe "https://example.com/callback" + } + + @Test + fun `selectRedirectUri returns null for empty list`() { + val redirectUris = emptyList() + val selectedUri = HostedUIHelper.selectRedirectUri(redirectUris) + selectedUri shouldBe null + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsContractTest.java b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsContractTest.java index 30b2fee54e..ebedc76575 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsContractTest.java +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsContractTest.java @@ -97,11 +97,34 @@ public void testCognitoOptions() { .scopes(scopes).build(); Assert.assertEquals(webUISignInOptions.getBrowserPackage(), "chrome"); Assert.assertEquals(webUISignInOptions.getScopes(), scopes); + Assert.assertNull(webUISignInOptions.getPreferPrivateSession()); + + // Test preferPrivateSession option + AWSCognitoAuthWebUISignInOptions webUISignInOptionsWithPrivateSession = + AWSCognitoAuthWebUISignInOptions.builder() + .browserPackage("chrome") + .scopes(scopes) + .preferPrivateSession(true) + .build(); + Assert.assertEquals("chrome", webUISignInOptionsWithPrivateSession.getBrowserPackage()); + Assert.assertEquals(scopes, webUISignInOptionsWithPrivateSession.getScopes()); + Assert.assertEquals(Boolean.TRUE, webUISignInOptionsWithPrivateSession.getPreferPrivateSession()); + + // Test preferPrivateSession set to false + AWSCognitoAuthWebUISignInOptions webUISignInOptionsWithoutPrivateSession = + AWSCognitoAuthWebUISignInOptions.builder() + .browserPackage("firefox") + .scopes(scopes) + .preferPrivateSession(false) + .build(); + Assert.assertEquals("firefox", webUISignInOptionsWithoutPrivateSession.getBrowserPackage()); + Assert.assertEquals(scopes, webUISignInOptionsWithoutPrivateSession.getScopes()); + Assert.assertEquals(Boolean.FALSE, webUISignInOptionsWithoutPrivateSession.getPreferPrivateSession()); FederateToIdentityPoolOptions federateToIdentityPoolOptions = FederateToIdentityPoolOptions.builder().developerProvidedIdentityId("test-idp") .build(); - Assert.assertEquals(federateToIdentityPoolOptions - .getDeveloperProvidedIdentityId(), "test-idp"); + Assert.assertEquals("test-idp", federateToIdentityPoolOptions + .getDeveloperProvidedIdentityId()); } } diff --git a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt index bec1041b76..12d4840bb9 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -58,7 +58,7 @@ class AndroidLibraryConventionPlugin : Plugin { } extension.apply { - compileSdk = 34 + compileSdk = 36 buildFeatures { buildConfig = true diff --git a/core/api/core.api b/core/api/core.api index 1e27a36ea9..50d0ae8547 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -1094,9 +1094,10 @@ public final class com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions$ } public class com/amplifyframework/auth/options/AuthWebUISignInOptions { - protected fun (Ljava/util/List;)V + protected fun (Ljava/util/List;Ljava/lang/Boolean;)V public static fun builder ()Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder; public fun equals (Ljava/lang/Object;)Z + public fun getPreferPrivateSession ()Ljava/lang/Boolean; public fun getScopes ()Ljava/util/List; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -1105,8 +1106,10 @@ public class com/amplifyframework/auth/options/AuthWebUISignInOptions { public abstract class com/amplifyframework/auth/options/AuthWebUISignInOptions$Builder { public fun ()V public fun build ()Lcom/amplifyframework/auth/options/AuthWebUISignInOptions; + public fun getPreferPrivateSession ()Ljava/lang/Boolean; public fun getScopes ()Ljava/util/List; public abstract fun getThis ()Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder; + public fun preferPrivateSession (Ljava/lang/Boolean;)Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder; public fun scopes (Ljava/util/List;)Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder; } diff --git a/core/src/main/java/com/amplifyframework/auth/options/AuthWebUISignInOptions.java b/core/src/main/java/com/amplifyframework/auth/options/AuthWebUISignInOptions.java index 2f0f644203..ae9b46ba81 100644 --- a/core/src/main/java/com/amplifyframework/auth/options/AuthWebUISignInOptions.java +++ b/core/src/main/java/com/amplifyframework/auth/options/AuthWebUISignInOptions.java @@ -16,6 +16,7 @@ package com.amplifyframework.auth.options; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; import com.amplifyframework.util.Immutable; @@ -29,13 +30,17 @@ */ public class AuthWebUISignInOptions { private final List scopes; + private final Boolean preferPrivateSession; /** * Advanced options for signing in with a hosted web UI. * @param scopes specify OAUTH scopes + * @param preferPrivateSession specifying whether or not to launch web ui in an ephemeral CustomTab. + * Default value is unset (null), which behaves the same as false. */ - protected AuthWebUISignInOptions(List scopes) { + protected AuthWebUISignInOptions(List scopes, Boolean preferPrivateSession) { this.scopes = scopes; + this.preferPrivateSession = preferPrivateSession; } /** @@ -47,6 +52,16 @@ public List getScopes() { return scopes; } + /** + * Optional override to prefer launching in an Ephemeral CustomTab, if available. + * @return optional override to prefer launching in an Ephemeral CustomTab, if available. + * Default value is unset (null), which behaves the same as false. + */ + @Nullable + public Boolean getPreferPrivateSession() { + return preferPrivateSession; + } + /** * Get a builder to construct an instance of this object. * @return a builder to construct an instance of this object. @@ -63,7 +78,8 @@ public static Builder builder() { @Override public int hashCode() { return ObjectsCompat.hash( - getScopes() + getScopes(), + getPreferPrivateSession() ); } @@ -79,7 +95,8 @@ public boolean equals(Object obj) { return false; } else { AuthWebUISignInOptions authWebUISignInOptions = (AuthWebUISignInOptions) obj; - return ObjectsCompat.equals(getScopes(), authWebUISignInOptions.getScopes()); + return ObjectsCompat.equals(getScopes(), authWebUISignInOptions.getScopes()) && + ObjectsCompat.equals(getPreferPrivateSession(), authWebUISignInOptions.getPreferPrivateSession()); } } @@ -91,6 +108,7 @@ public boolean equals(Object obj) { public String toString() { return "AuthWebUISignInOptions{" + "scopes=" + getScopes() + + ", preferPrivateSession=" + getPreferPrivateSession() + '}'; } @@ -100,6 +118,7 @@ public String toString() { */ public abstract static class Builder> { private List scopes; + private Boolean preferPrivateSession; /** * Initialize the builder object with fields initialized with empty collection objects. @@ -122,6 +141,16 @@ public List getScopes() { return scopes; } + /** + * Optional override to prefer launching in an Ephemeral CustomTab, if available. + * @return optional override to prefer launching in an Ephemeral CustomTab, if available. + * Default value is unset (null), which behaves the same as false. + */ + @Nullable + public Boolean getPreferPrivateSession() { + return preferPrivateSession; + } + /** * Map of custom parameters to send associated with sign in process. * @param scopes specify OAUTH scopes @@ -135,6 +164,18 @@ public T scopes(@NonNull List scopes) { return getThis(); } + /** + * This can optionally be set to prefer launching in an Ephemeral CustomTab, if available. + * + * @param preferPrivateSession Boolean specifying whether or not to launch web ui in an + * ephemeral CustomTab. Default value is unset (null), which behaves the same as false. + * @return the instance of the builder. + */ + public T preferPrivateSession(@NonNull Boolean preferPrivateSession) { + this.preferPrivateSession = preferPrivateSession; + return getThis(); + } + /** * Build an instance of AuthWebUISignInOptions (or one of its subclasses). * @return an instance of AuthWebUISignInOptions (or one of its subclasses) @@ -142,7 +183,8 @@ public T scopes(@NonNull List scopes) { @NonNull public AuthWebUISignInOptions build() { return new AuthWebUISignInOptions( - Immutable.of(scopes) + Immutable.of(scopes), + preferPrivateSession ); } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5d8fea582..001593dda2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ androidx-activity = "1.2.0" androidx-annotation = "1.9.1" androidx-annotation-experimental = "1.4.1" androidx-appcompat = "1.2.0" -androidx-browser = "1.4.0" +androidx-browser = "1.9.0" androidx-concurrent = "1.1.0" androidx-core = "1.5.0" androidx-credentials = "1.3.0"