Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -47,6 +47,7 @@ public class Features {
public static final String FEATURE_WELCOME_DISCOVERY_LOGIN = "WD";
public static final String FEATURE_RTR = "RT";
public static final String FEATURE_DPOP = "DP";
public static final String FEATURE_APP_ATTESTATION = "AA";

// "Why browser login was used" — registered per-user alongside FEATURE_BROWSER_LOGIN (BW)
public static final String FEATURE_BROWSER_LOGIN_SERVER_AUTH_CONFIG = "B1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,13 @@ open class SalesforceSDKManager protected constructor(
rtrActive = currentUser != null && isUserFeatureRegistered(Features.FEATURE_RTR, currentUser),
)
)
additionalSections.add(
DevSupportInfo.parseAppAttestationSection(
appAttestationClient = appAttestationClient,
currentUser = currentUser,
aaFeatureActive = currentUser != null && isUserFeatureRegistered(Features.FEATURE_APP_ATTESTATION, currentUser),
)
)
}

/** Sends the logout completed intent */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ package com.salesforce.androidsdk.developer.support

import com.salesforce.androidsdk.accounts.UserAccount
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.auth.AppAttestationClient
import com.salesforce.androidsdk.auth.JwtAccessToken
import com.salesforce.androidsdk.auth.dpop.DPoPKeyManager
import com.salesforce.androidsdk.auth.dpop.DPoPNonceCache
Expand Down Expand Up @@ -200,6 +201,44 @@ data class DevSupportInfo(
* Per-user fields show "N/A" when there is no current user; "Last
* Rotation" shows "Never" until the first confirmed rotation.
*/
/**
* Builds the "App Attestation" section for the developer info screen.
*
* @param appAttestationClient The app attestation client, or null if not configured.
* @param currentUser The current user account, or null if no user is logged in.
* @param aaFeatureActive True if the AA feature flag is registered for the current user.
* @return An "App Attestation" section with rows for enabled state, API host,
* Google Cloud Project ID, integrity provider readiness, and feature flag.
*/
internal fun parseAppAttestationSection(
appAttestationClient: AppAttestationClient?,
currentUser: UserAccount?,
aaFeatureActive: Boolean,
): DevInfoSection {
val attestationEnabled = appAttestationClient != null && appAttestationClient.apiHostName != null
val apiHost = appAttestationClient?.apiHostName ?: "N/A"
val gcpProjectId = if (appAttestationClient != null) {
appAttestationClient.googleCloudProjectId.toString()
} else "N/A"
val providerReady = when {
appAttestationClient == null -> "N/A"
appAttestationClient.integrityTokenProvider != null -> "true"

@github-actions github-actions Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ This method should only be accessed from tests or within private scope

else -> "false"
}
val featureFlag = when {
currentUser == null -> "N/A"
aaFeatureActive -> "true"
else -> "false"
}
return "App Attestation" to listOf(
"Attestation Enabled" to attestationEnabled.toString(),
"API Host" to apiHost,
"Google Cloud Project ID" to gcpProjectId,
"Integrity Provider Ready" to providerReady,
"Feature Flag (AA)" to featureFlag,
)
}

internal fun parseRtrSection(currentUser: UserAccount?, rtrActive: Boolean) =
if (currentUser == null) {
"RTR" to listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ package com.salesforce.androidsdk.developer.support
import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.salesforce.androidsdk.accounts.UserAccount
import com.salesforce.androidsdk.auth.AppAttestationClient
import com.salesforce.androidsdk.auth.dpop.DPoPProofBuilder
import com.salesforce.androidsdk.config.BootConfig
import com.salesforce.androidsdk.config.RuntimeConfig
Expand Down Expand Up @@ -777,6 +778,111 @@ class DevSupportInfoTest {
assertEquals(timestamp, rows.find { it.first == "Last Rotation" }?.second)
}

// App Attestation section

@Test
fun appAttestationSection_NoClient_AttestationDisabledAllNA() {
val (title, rows) = DevSupportInfo.parseAppAttestationSection(
appAttestationClient = null,
currentUser = null,
aaFeatureActive = false,
)

assertEquals("App Attestation", title)
assertEquals("false", rows.find { it.first == "Attestation Enabled" }?.second)
assertEquals("N/A", rows.find { it.first == "API Host" }?.second)
assertEquals("N/A", rows.find { it.first == "Google Cloud Project ID" }?.second)
assertEquals("N/A", rows.find { it.first == "Integrity Provider Ready" }?.second)
assertEquals("N/A", rows.find { it.first == "Feature Flag (AA)" }?.second)
}

@Test
fun appAttestationSection_ClientWithoutHost_AttestationDisabled() {
val client = createMockAppAttestationClient(apiHostName = null, integrityProviderReady = false)

val (_, rows) = DevSupportInfo.parseAppAttestationSection(
appAttestationClient = client,
currentUser = null,
aaFeatureActive = false,
)

assertEquals("false", rows.find { it.first == "Attestation Enabled" }?.second)
assertEquals("N/A", rows.find { it.first == "API Host" }?.second)
}

@Test
fun appAttestationSection_ClientConfigured_ProviderNotReady() {
val client = createMockAppAttestationClient(apiHostName = "myorg.my.salesforce.com", integrityProviderReady = false)

val (_, rows) = DevSupportInfo.parseAppAttestationSection(
appAttestationClient = client,
currentUser = null,
aaFeatureActive = false,
)

assertEquals("true", rows.find { it.first == "Attestation Enabled" }?.second)
assertEquals("myorg.my.salesforce.com", rows.find { it.first == "API Host" }?.second)
assertEquals("false", rows.find { it.first == "Integrity Provider Ready" }?.second)
}

@Test
fun appAttestationSection_FullyConfigured_AllFieldsPopulated() {
val client = createMockAppAttestationClient(
apiHostName = "myorg.my.salesforce.com",
googleCloudProjectId = 123456789L,
integrityProviderReady = true,
)

val (title, rows) = DevSupportInfo.parseAppAttestationSection(
appAttestationClient = client,
currentUser = null,
aaFeatureActive = false,
)

assertEquals("App Attestation", title)
assertEquals("true", rows.find { it.first == "Attestation Enabled" }?.second)
assertEquals("myorg.my.salesforce.com", rows.find { it.first == "API Host" }?.second)
assertEquals("123456789", rows.find { it.first == "Google Cloud Project ID" }?.second)
assertEquals("true", rows.find { it.first == "Integrity Provider Ready" }?.second)
}

@Test
fun appAttestationSection_FeatureFlag_NoUser_ShowsNA() {
val (_, rows) = DevSupportInfo.parseAppAttestationSection(
appAttestationClient = null,
currentUser = null,
aaFeatureActive = false,
)

assertEquals("N/A", rows.find { it.first == "Feature Flag (AA)" }?.second)
}

@Test
fun appAttestationSection_FeatureFlag_UserWithoutFlag_ShowsFalse() {
val user = createMockUserAccount()

val (_, rows) = DevSupportInfo.parseAppAttestationSection(
appAttestationClient = null,
currentUser = user,
aaFeatureActive = false,
)

assertEquals("false", rows.find { it.first == "Feature Flag (AA)" }?.second)
}

@Test
fun appAttestationSection_FeatureFlag_UserWithFlag_ShowsTrue() {
val user = createMockUserAccount()

val (_, rows) = DevSupportInfo.parseAppAttestationSection(
appAttestationClient = null,
currentUser = user,
aaFeatureActive = true,
)

assertEquals("true", rows.find { it.first == "Feature Flag (AA)" }?.second)
}

// Helper methods

private fun createMockRuntimeConfig(
Expand Down Expand Up @@ -844,6 +950,18 @@ class DevSupportInfoTest {
)
}

private fun createMockAppAttestationClient(
apiHostName: String? = null,
googleCloudProjectId: Long = 0L,
integrityProviderReady: Boolean = false,
): AppAttestationClient {
return mockk<AppAttestationClient>(relaxed = true) {
every { this@mockk.apiHostName } returns apiHostName
every { this@mockk.googleCloudProjectId } returns googleCloudProjectId
every { integrityTokenProvider } returns if (integrityProviderReady) mockk(relaxed = true) else null
}
}

private fun createMockJwtToken(expirationTime: Long): String {
// Create a simple JWT token with the expiration time
// JWT format: header.payload.signature
Expand Down
Loading