Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -5,11 +5,14 @@ import okhttp3.Request
/** Mock provider for authentication and app upgrade endpoints. */
object MockAuthProvider {

fun mockLogin(request: Request): String = """
{"success":true,"code":200,"message":"success","data":{
"token":"mock_jwt_${System.currentTimeMillis()}"
}}
""".trimIndent()
fun mockLogin(request: Request): String {
MockCampusCredentialProvider.resetForLogin(request)
return """
{"success":true,"code":200,"message":"success","data":{
"token":"mock_jwt_${System.currentTimeMillis()}"
}}
""".trimIndent()
}

fun mockUpgrade(request: Request): String = """
{"success":true,"code":200,"message":"","data":{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ object MockCampusCredentialProvider {

private var state = MockCampusCredentialState()

fun resetForLogin(request: Request) {
val body = request.jsonObjectBody()
val hasConsent = body?.getBoolean("campusCredentialConsent") ?: true
state = if (hasConsent) {
MockCampusCredentialState()
} else {
MockCampusCredentialState(
hasActiveConsent = false,
hasSavedCredential = false,
quickAuthAllowed = false,
consentedAt = null,
campusAccount = null
)
}
}

fun mockStatus(request: Request): String = MockUtils.successDataJson(state.toPayload())

fun mockConsent(request: Request): String {
Expand Down
158 changes: 102 additions & 56 deletions app/src/main/java/cn/gdeiassistant/ui/profile/AccountCenterScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import cn.gdeiassistant.R
import cn.gdeiassistant.model.CampusCredentialStatus
import cn.gdeiassistant.model.PhoneAttribution
import cn.gdeiassistant.model.PrivacySettings
import cn.gdeiassistant.model.UserDataExportState
Expand Down Expand Up @@ -900,16 +901,16 @@ private fun CampusCredentialManagementCard(
onDeleteClick: () -> Unit
) {
val status = state.campusCredentialStatus
val quickAuthActionText = if (status.quickAuthEnabled) {
stringResource(R.string.profile_settings_campus_credentials_quick_auth_disable_action)
} else {
stringResource(R.string.profile_settings_campus_credentials_quick_auth_enable_action)
}
val quickAuthSubtitle = if (status.quickAuthEnabled) {
stringResource(R.string.profile_settings_campus_credentials_quick_auth_enabled_subtitle)
} else {
stringResource(R.string.profile_settings_campus_credentials_quick_auth_disabled_subtitle)
}
val canRunCredentialAction = !state.isCampusCredentialLoading &&
!state.isCampusCredentialActionRunning &&
!state.isBackendTargetChanging
val canToggleQuickAuth = canRunCredentialAction &&
(status.quickAuthEnabled || (status.hasActiveConsent && status.hasSavedCredential))

SectionCard(modifier = Modifier.fillMaxWidth()) {
Text(
Expand Down Expand Up @@ -943,40 +944,10 @@ private fun CampusCredentialManagementCard(
Spacer(modifier = Modifier.height(12.dp))
}

SettingInfoRow(
title = stringResource(R.string.profile_settings_campus_credentials_consent_status_label),
value = stringResource(
if (status.hasActiveConsent) {
R.string.profile_settings_campus_credentials_status_authorized
} else {
R.string.profile_settings_campus_credentials_status_unauthorized
}
)
)
Spacer(modifier = Modifier.height(10.dp))
SettingInfoRow(
title = stringResource(R.string.profile_settings_campus_credentials_saved_label),
value = stringResource(
if (status.hasSavedCredential) {
R.string.profile_settings_campus_credentials_boolean_yes
} else {
R.string.profile_settings_campus_credentials_boolean_no
}
)
)
Spacer(modifier = Modifier.height(10.dp))
SettingInfoRow(
title = stringResource(R.string.profile_settings_campus_credentials_quick_auth_label),
value = stringResource(
if (status.quickAuthEnabled) {
R.string.profile_settings_campus_credentials_quick_auth_on
} else {
R.string.profile_settings_campus_credentials_quick_auth_off
}
)
)
CredentialStatusSummary(status = status)
Spacer(modifier = Modifier.height(16.dp))

status.maskedCampusAccount?.takeIf(String::isNotBlank)?.let { maskedAccount ->
Spacer(modifier = Modifier.height(10.dp))
SettingInfoRow(
title = stringResource(R.string.profile_settings_campus_credentials_account_label),
value = maskedAccount
Expand All @@ -997,29 +968,27 @@ private fun CampusCredentialManagementCard(
)
}

Spacer(modifier = Modifier.height(16.dp))
TintButton(
text = quickAuthActionText,
onClick = { onToggleQuickAuth(!status.quickAuthEnabled) },
enabled = !state.isCampusCredentialLoading &&
!state.isCampusCredentialActionRunning &&
!state.isBackendTargetChanging,
modifier = Modifier.fillMaxWidth()
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
SettingSwitchRow(
title = stringResource(R.string.profile_settings_campus_credentials_quick_auth_label),
subtitle = quickAuthSubtitle,
checked = status.quickAuthEnabled,
enabled = canToggleQuickAuth,
onCheckedChange = onToggleQuickAuth
)
Spacer(modifier = Modifier.height(10.dp))
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text(
text = quickAuthSubtitle,
style = MaterialTheme.typography.bodySmall,
text = stringResource(R.string.profile_settings_campus_credentials_danger_title),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
GhostButton(
text = stringResource(R.string.profile_settings_campus_credentials_revoke_action),
icon = Icons.Rounded.WarningAmber,
onClick = onRevokeClick,
enabled = !state.isCampusCredentialLoading &&
!state.isCampusCredentialActionRunning &&
!state.isBackendTargetChanging,
enabled = canRunCredentialAction,
modifier = Modifier.fillMaxWidth(),
borderColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.28f),
contentColor = MaterialTheme.colorScheme.tertiary
Expand All @@ -1029,16 +998,93 @@ private fun CampusCredentialManagementCard(
text = stringResource(R.string.profile_settings_campus_credentials_delete_action),
icon = Icons.Rounded.DeleteForever,
onClick = onDeleteClick,
enabled = !state.isCampusCredentialLoading &&
!state.isCampusCredentialActionRunning &&
!state.isBackendTargetChanging,
enabled = canRunCredentialAction,
modifier = Modifier.fillMaxWidth(),
borderColor = MaterialTheme.colorScheme.error.copy(alpha = 0.28f),
contentColor = MaterialTheme.colorScheme.error
)
}
}

@Composable
private fun CredentialStatusSummary(status: CampusCredentialStatus) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
CredentialStatusChip(
label = stringResource(R.string.profile_settings_campus_credentials_consent_status_label),
value = stringResource(
if (status.hasActiveConsent) {
R.string.profile_settings_campus_credentials_status_authorized
} else {
R.string.profile_settings_campus_credentials_status_unauthorized
}
),
active = status.hasActiveConsent,
modifier = Modifier.weight(1f)
)
CredentialStatusChip(
label = stringResource(R.string.profile_settings_campus_credentials_saved_label),
value = stringResource(
if (status.hasSavedCredential) {
R.string.profile_settings_campus_credentials_boolean_yes
} else {
R.string.profile_settings_campus_credentials_boolean_no
}
),
active = status.hasSavedCredential,
modifier = Modifier.weight(1f)
)
CredentialStatusChip(
label = stringResource(R.string.profile_settings_campus_credentials_quick_auth_label),
value = stringResource(
if (status.quickAuthEnabled) {
R.string.profile_settings_campus_credentials_quick_auth_on
} else {
R.string.profile_settings_campus_credentials_quick_auth_off
}
),
active = status.quickAuthEnabled,
modifier = Modifier.weight(1f)
)
}
}

@Composable
private fun CredentialStatusChip(
label: String,
value: String,
active: Boolean,
modifier: Modifier = Modifier
) {
val tint = if (active) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
modifier = modifier,
shape = RoundedCornerShape(12.dp),
color = tint.copy(alpha = if (active) 0.10f else 0.06f)
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 10.dp)) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = value,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold,
color = tint
)
}
}
}

@Composable
private fun CampusCredentialConfirmationDialog(
title: String,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-en/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,7 @@
<string name="profile_settings_toggle_failed">Failed to toggle setting</string>
<string name="profile_settings_campus_credentials_title">Campus Credential Management</string>
<string name="profile_settings_campus_credentials_subtitle">Review consent status, saved credentials, and quick authentication, and revoke consent or delete saved credentials at any time.</string>
<string name="profile_settings_campus_credentials_danger_title">Danger Zone</string>
<string name="profile_settings_campus_credentials_loading">Syncing campus credential status…</string>
<string name="profile_settings_campus_credentials_consent_status_label">Consent status</string>
<string name="profile_settings_campus_credentials_saved_label">Saved credential</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,7 @@
<string name="profile_settings_toggle_failed">設定の切り替えに失敗しました</string>
<string name="profile_settings_campus_credentials_title">キャンパス資格情報の管理</string>
<string name="profile_settings_campus_credentials_subtitle">同意状況、保存済み資格情報、クイック認証を確認し、いつでも同意撤回や保存済み資格情報の削除を行えます。</string>
<string name="profile_settings_campus_credentials_danger_title">危険な操作</string>
<string name="profile_settings_campus_credentials_loading">キャンパス資格情報の状態を同期しています…</string>
<string name="profile_settings_campus_credentials_consent_status_label">同意状況</string>
<string name="profile_settings_campus_credentials_saved_label">保存済み資格情報</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-ko/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,7 @@
<string name="profile_settings_toggle_failed">설정 전환 실패</string>
<string name="profile_settings_campus_credentials_title">캠퍼스 자격 정보 관리</string>
<string name="profile_settings_campus_credentials_subtitle">동의 상태, 저장된 자격 정보, 빠른 인증을 확인하고 언제든지 동의를 철회하거나 저장된 자격 정보를 삭제할 수 있습니다.</string>
<string name="profile_settings_campus_credentials_danger_title">위험한 작업</string>
<string name="profile_settings_campus_credentials_loading">캠퍼스 자격 정보 상태를 동기화하는 중…</string>
<string name="profile_settings_campus_credentials_consent_status_label">동의 상태</string>
<string name="profile_settings_campus_credentials_saved_label">저장된 자격 정보</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-zh-rHK/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,7 @@
<string name="profile_settings_toggle_failed">切換設置失敗</string>
<string name="profile_settings_campus_credentials_title">校園憑證管理</string>
<string name="profile_settings_campus_credentials_subtitle">查看授權狀態、已保存憑證和快速認證,並可隨時撤回授權或刪除已保存憑證。</string>
<string name="profile_settings_campus_credentials_danger_title">危險操作</string>
<string name="profile_settings_campus_credentials_loading">正在同步校園憑證狀態…</string>
<string name="profile_settings_campus_credentials_consent_status_label">授權狀態</string>
<string name="profile_settings_campus_credentials_saved_label">已保存憑證</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,7 @@
<string name="profile_settings_toggle_failed">切換設定失敗</string>
<string name="profile_settings_campus_credentials_title">校園憑證管理</string>
<string name="profile_settings_campus_credentials_subtitle">查看授權狀態、已保存憑證和快速認證,並可隨時撤回授權或刪除已保存憑證。</string>
<string name="profile_settings_campus_credentials_danger_title">危險操作</string>
<string name="profile_settings_campus_credentials_loading">正在同步校園憑證狀態…</string>
<string name="profile_settings_campus_credentials_consent_status_label">授權狀態</string>
<string name="profile_settings_campus_credentials_saved_label">已保存憑證</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,7 @@
<string name="profile_settings_toggle_failed">切换设置失败</string>
<string name="profile_settings_campus_credentials_title">校园凭证管理</string>
<string name="profile_settings_campus_credentials_subtitle">查看授权状态、已保存凭证和快速认证,并可随时撤回授权或删除已保存凭证。</string>
<string name="profile_settings_campus_credentials_danger_title">危险操作</string>
<string name="profile_settings_campus_credentials_loading">正在同步校园凭证状态…</string>
<string name="profile_settings_campus_credentials_consent_status_label">授权状态</string>
<string name="profile_settings_campus_credentials_saved_label">已保存凭证</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,45 @@ class MockInterceptorSmokeTest {
val privacy = executeJson("/api/privacy")
assertTrue(privacy.dataObject().has("cacheAllow"))

val campusCredentialStatus = executeJson("/api/campus-credential/status")
assertTrue(campusCredentialStatus.dataObject().get("hasActiveConsent").asBoolean)
assertTrue(campusCredentialStatus.dataObject().get("hasSavedCredential").asBoolean)
assertTrue(campusCredentialStatus.dataObject().get("quickAuthEnabled").asBoolean)
assertTrue(campusCredentialStatus.dataObject().get("maskedCampusAccount").asString.isNotBlank())

val quickAuthOff = executeJson(
path = "/api/campus-credential/quick-auth",
method = "POST",
jsonBody = """{"enabled":false}"""
)
assertEquals(false, quickAuthOff.dataObject().get("quickAuthEnabled").asBoolean)

val revoked = executeJson(path = "/api/campus-credential/revoke", method = "POST")
assertEquals(false, revoked.dataObject().get("hasActiveConsent").asBoolean)
assertEquals(false, revoked.dataObject().get("hasSavedCredential").asBoolean)

val deleted = executeJson(path = "/api/campus-credential", method = "DELETE")
assertEquals(false, deleted.dataObject().get("quickAuthEnabled").asBoolean)

executeJson(
path = "/api/auth/login",
method = "POST",
jsonBody = """{"username":"gdeiassistant","password":"gdeiassistant"}"""
)
val campusCredentialStatusAfterRelogin = executeJson("/api/campus-credential/status")
assertTrue(campusCredentialStatusAfterRelogin.dataObject().get("hasActiveConsent").asBoolean)
assertTrue(campusCredentialStatusAfterRelogin.dataObject().get("hasSavedCredential").asBoolean)

executeJson(
path = "/api/auth/login",
method = "POST",
jsonBody = """{"username":"gdeiassistant","password":"gdeiassistant","campusCredentialConsent":false}"""
)
val campusCredentialStatusWithoutConsent = executeJson("/api/campus-credential/status")
assertEquals(false, campusCredentialStatusWithoutConsent.dataObject().get("hasActiveConsent").asBoolean)
assertEquals(false, campusCredentialStatusWithoutConsent.dataObject().get("hasSavedCredential").asBoolean)
assertEquals("", campusCredentialStatusWithoutConsent.dataObject().get("maskedCampusAccount").asString)

val phoneStatus = executeJson("/api/phone/status")
assertTrue(phoneStatus.dataObject().get("phone").asString.isNotBlank())

Expand Down