Skip to content

Commit a9b7040

Browse files
Neelansh Sahaineelanshsahai
authored andcommitted
Add Signal API implementation (RP)
1 parent dc9711a commit a9b7040

File tree

17 files changed

+349
-15
lines changed

17 files changed

+349
-15
lines changed

Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,19 @@ import androidx.credentials.GetCredentialResponse
3333
import androidx.credentials.GetPasswordOption
3434
import androidx.credentials.GetPublicKeyCredentialOption
3535
import androidx.credentials.GetRestoreCredentialOption
36+
import androidx.credentials.SignalAllAcceptedCredentialIdsRequest
37+
import androidx.credentials.SignalCurrentUserDetailsRequest
38+
import androidx.credentials.SignalUnknownCredentialRequest
3639
import androidx.credentials.exceptions.CreateCredentialException
3740
import androidx.credentials.exceptions.GetCredentialCancellationException
3841
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
3942
import com.authentication.shrine.repository.SERVER_CLIENT_ID
4043
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
44+
import androidx.datastore.core.DataStore
45+
import androidx.datastore.preferences.core.Preferences
46+
import com.authentication.shrine.repository.AuthRepository.Companion.RP_ID_KEY
47+
import com.authentication.shrine.repository.AuthRepository.Companion.USER_ID_KEY
48+
import com.authentication.shrine.repository.AuthRepository.Companion.read
4149
import org.json.JSONObject
4250
import javax.inject.Inject
4351

@@ -48,6 +56,7 @@ import javax.inject.Inject
4856
*/
4957
class CredentialManagerUtils @Inject constructor(
5058
private val credentialManager: CredentialManager,
59+
private val dataStore: DataStore<Preferences>,
5160
) {
5261

5362
/**
@@ -249,6 +258,40 @@ class CredentialManagerUtils @Inject constructor(
249258
val clearRequest = ClearCredentialStateRequest(requestType = TYPE_CLEAR_RESTORE_CREDENTIAL)
250259
credentialManager.clearCredentialState(clearRequest)
251260
}
261+
262+
@SuppressLint("RestrictedApi")
263+
suspend fun signalUnknown(
264+
credentialId: String,
265+
) {
266+
credentialManager.signalCredentialState(
267+
SignalUnknownCredentialRequest(
268+
"""{"rpId":"${dataStore.read(RP_ID_KEY)}", "credentialId":"$credentialId"}""",
269+
),
270+
)
271+
}
272+
273+
@SuppressLint("RestrictedApi")
274+
suspend fun signalAcceptedIds(
275+
credentialIds: List<String>,
276+
) {
277+
credentialManager.signalCredentialState(
278+
SignalAllAcceptedCredentialIdsRequest(
279+
"""{"rpId":"${dataStore.read(RP_ID_KEY)}","userId":"${dataStore.read(USER_ID_KEY)}","allAcceptedCredentialIds":["${credentialIds.joinToString(",")}"]}""",
280+
),
281+
)
282+
}
283+
284+
@SuppressLint("RestrictedApi")
285+
suspend fun signalUserDetails(
286+
newName: String,
287+
newDisplayName: String,
288+
) {
289+
credentialManager.signalCredentialState(
290+
SignalCurrentUserDetailsRequest(
291+
"""{"rpId":"${dataStore.read(RP_ID_KEY)}","userId":"${dataStore.read(USER_ID_KEY)}", "name":"$newName","displayName":"$newDisplayName"}""",
292+
),
293+
)
294+
}
252295
}
253296

254297
sealed class GenericCredentialManagerResponse {

Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ object AppModule {
109109
@Provides
110110
fun providesCredentialManagerUtils(
111111
credentialManager: CredentialManager,
112+
dataStore: DataStore<Preferences>,
112113
): CredentialManagerUtils {
113-
return CredentialManagerUtils(credentialManager)
114+
return CredentialManagerUtils(
115+
credentialManager = credentialManager,
116+
dataStore = dataStore,
117+
)
114118
}
115119

116120
@Singleton

Shrine/app/src/main/java/com/authentication/shrine/model/PasskeysList.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ data class PasskeyCredential(
4747
val aaguid: String,
4848
val registeredAt: Long,
4949
val providerIcon: String,
50+
val isSelected: Boolean = false,
5051
)

Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ class AuthRepository @Inject constructor(
7878
val USERNAME = stringPreferencesKey("username")
7979
val IS_SIGNED_IN_THROUGH_PASSKEYS = booleanPreferencesKey("is_signed_passkeys")
8080
val SESSION_ID = stringPreferencesKey("session_id")
81+
val RP_ID_KEY = stringPreferencesKey("rp_id_key")
82+
val USER_ID_KEY = stringPreferencesKey("user_id_key")
83+
val CRED_ID = stringPreferencesKey("cred_id")
8184
val RESTORE_KEY_CREDENTIAL_ID = stringPreferencesKey("restore_key_credential_id")
8285

8386
// Value for restore credential AuthApiService parameter
@@ -151,7 +154,6 @@ class AuthRepository @Inject constructor(
151154
}
152155
}
153156

154-
155157
/**
156158
* Signs in with a password.
157159
*
@@ -217,6 +219,7 @@ class AuthRepository @Inject constructor(
217219
)
218220
if (response.isSuccessful) {
219221
dataStore.edit { prefs ->
222+
prefs[RP_ID_KEY] = response.body()?.rp?.id ?: ""
220223
response.getSessionId()?.also {
221224
prefs[SESSION_ID] = it
222225
}
@@ -287,6 +290,7 @@ class AuthRepository @Inject constructor(
287290
)
288291
if (apiResult.isSuccessful) {
289292
dataStore.edit { prefs ->
293+
prefs[CRED_ID] = rawId
290294
if (credentialResponse is CreateRestoreCredentialResponse) {
291295
prefs[RESTORE_KEY_CREDENTIAL_ID] = rawId
292296
}
@@ -321,6 +325,7 @@ class AuthRepository @Inject constructor(
321325
val response = authApiService.signInRequest()
322326
if (response.isSuccessful) {
323327
dataStore.edit { prefs ->
328+
prefs[RP_ID_KEY] = response.body()?.rpId ?: ""
324329
response.getSessionId()?.also {
325330
prefs[SESSION_ID] = it
326331
}
@@ -382,6 +387,7 @@ class AuthRepository @Inject constructor(
382387
)
383388
return if (apiResult.isSuccessful) {
384389
dataStore.edit { prefs ->
390+
prefs[CRED_ID] = credentialId
385391
apiResult.getSessionId()?.also {
386392
prefs[SESSION_ID] = it
387393
}
@@ -545,6 +551,9 @@ class AuthRepository @Inject constructor(
545551
cookie = sessionId.createCookieHeader(),
546552
)
547553
if (apiResult.isSuccessful) {
554+
dataStore.edit { prefs ->
555+
prefs[USER_ID_KEY] = apiResult.body()?.userId ?: ""
556+
}
548557
return apiResult.body()
549558
} else if (apiResult.code() == 401) {
550559
signOut()

Shrine/app/src/main/java/com/authentication/shrine/ui/MainMenuScreen.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ fun MainMenuScreen(
5555
onSettingsButtonClicked: () -> Unit,
5656
onHelpButtonClicked: () -> Unit,
5757
navigateToLogin: () -> Unit,
58+
navigateToUpdateProfile: () -> Unit,
5859
viewModel: HomeViewModel,
5960
modifier: Modifier = Modifier,
6061
credentialManagerUtils: CredentialManagerUtils,
@@ -71,6 +72,7 @@ fun MainMenuScreen(
7172
onHelpButtonClicked = onHelpButtonClicked,
7273
navigateToLogin = navigateToLogin,
7374
onSignOut = onSignOut,
75+
navigateToUpdateProfile = navigateToUpdateProfile,
7476
modifier = modifier,
7577
)
7678
}
@@ -93,6 +95,7 @@ fun MainMenuScreen(
9395
onSettingsButtonClicked: () -> Unit,
9496
onHelpButtonClicked: () -> Unit,
9597
navigateToLogin: () -> Unit,
98+
navigateToUpdateProfile: () -> Unit,
9699
onSignOut: () -> Unit,
97100
modifier: Modifier = Modifier,
98101
) {
@@ -121,6 +124,7 @@ fun MainMenuScreen(
121124
onHelpButtonClicked,
122125
onSignOut,
123126
navigateToLogin,
127+
navigateToUpdateProfile,
124128
)
125129
}
126130
}
@@ -142,6 +146,7 @@ private fun MainMenuButtonsList(
142146
onHelpButtonClicked: () -> Unit,
143147
onSignOut: () -> Unit,
144148
navigateToLogin: () -> Unit,
149+
navigateToUpdateProfile: () -> Unit,
145150
modifier: Modifier = Modifier,
146151
) {
147152
Column(
@@ -172,6 +177,11 @@ private fun MainMenuButtonsList(
172177
buttonText = stringResource(R.string.sign_out),
173178
usePrimaryColor = false,
174179
)
180+
181+
ShrineButton(
182+
onClick = navigateToUpdateProfile,
183+
buttonText = "Update Profile",
184+
)
175185
}
176186
}
177187

@@ -188,6 +198,7 @@ fun PasskeysSignedPreview() {
188198
onHelpButtonClicked = { },
189199
navigateToLogin = { },
190200
onSignOut = { },
201+
navigateToUpdateProfile = { },
191202
)
192203
}
193204
}

Shrine/app/src/main/java/com/authentication/shrine/ui/PasskeyManagementScreen.kt

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.compose.foundation.BorderStroke
1919
import androidx.compose.foundation.Image
2020
import androidx.compose.foundation.background
2121
import androidx.compose.foundation.border
22+
import androidx.compose.foundation.clickable
2223
import androidx.compose.foundation.layout.Arrangement
2324
import androidx.compose.foundation.layout.Column
2425
import androidx.compose.foundation.layout.Row
@@ -29,6 +30,7 @@ import androidx.compose.foundation.layout.size
2930
import androidx.compose.foundation.lazy.LazyColumn
3031
import androidx.compose.foundation.lazy.itemsIndexed
3132
import androidx.compose.foundation.shape.RoundedCornerShape
33+
import androidx.compose.material3.Checkbox
3234
import androidx.compose.material3.HorizontalDivider
3335
import androidx.compose.material3.MaterialTheme
3436
import androidx.compose.material3.Scaffold
@@ -94,7 +96,22 @@ fun PasskeyManagementScreen(
9496
requestResult = data,
9597
context = context,
9698
)
97-
})
99+
},
100+
)
101+
}
102+
}
103+
104+
val passkeysList = uiState.passkeysList
105+
val onItemClick = { index: Int ->
106+
viewModel.updateItem(index, passkeysList)
107+
}
108+
109+
val onSignalBtnClicked = {
110+
val credentialIdsList = passkeysList
111+
.filter { it.isSelected }
112+
.map { it.id }
113+
if (credentialIdsList.isNotEmpty()) {
114+
viewModel.signalAccepted(credentialIdsList)
98115
}
99116
}
100117

@@ -104,8 +121,10 @@ fun PasskeyManagementScreen(
104121
onCreatePasskeyClicked = onCreatePasskeyClicked,
105122
onDeleteClicked = onDeleteClicked,
106123
uiState = uiState,
107-
passkeysList = uiState.passkeysList,
124+
passkeysList = passkeysList,
108125
aaguidData = uiState.aaguidData,
126+
onItemClick = onItemClick,
127+
onSignal = onSignalBtnClicked,
109128
modifier = modifier,
110129
)
111130
}
@@ -126,6 +145,8 @@ fun PasskeyManagementScreen(
126145
uiState: PasskeyManagementUiState,
127146
passkeysList: List<PasskeyCredential>,
128147
aaguidData: Map<String, Map<String, String>>,
148+
onItemClick: (Int) -> Unit,
149+
onSignal: () -> Unit,
129150
modifier: Modifier = Modifier,
130151
) {
131152
val snackbarHostState = remember { SnackbarHostState() }
@@ -161,6 +182,13 @@ fun PasskeyManagementScreen(
161182
onDeleteClicked = onDeleteClicked,
162183
passkeysList = passkeysList,
163184
aaguidData = aaguidData,
185+
onItemClick = onItemClick,
186+
)
187+
188+
ShrineButton(
189+
onClick = onSignal,
190+
buttonText = "Signal Accepted",
191+
modifier = Modifier.fillMaxWidth()
164192
)
165193
} else {
166194
ShrineButton(
@@ -185,7 +213,7 @@ fun PasskeyManagementScreen(
185213
if (snackbarMessage != null) {
186214
LaunchedEffect(snackbarMessage) {
187215
snackbarHostState.showSnackbar(
188-
message = snackbarMessage
216+
message = snackbarMessage,
189217
)
190218
}
191219
}
@@ -202,6 +230,7 @@ fun PasskeysListColumn(
202230
onDeleteClicked: (credentialId: String) -> Unit,
203231
passkeysList: List<PasskeyCredential>,
204232
aaguidData: Map<String, Map<String, String>>,
233+
onItemClick: (Int) -> Unit,
205234
) {
206235
val shape = RoundedCornerShape(dimensionResource(R.dimen.padding_small))
207236
LazyColumn(
@@ -222,13 +251,15 @@ fun PasskeysListColumn(
222251
iconSvgString = aaguidData[item.aaguid]?.get("icon_light"),
223252
credentialProviderName = item.name,
224253
passkeyCreationDate = item.registeredAt.toReadableDate(),
254+
isChecked = item.isSelected,
255+
modifier = Modifier.clickable { onItemClick(index) }
225256
)
226257

227258
if (index < passkeysList.lastIndex) {
228259
HorizontalDivider(
229260
modifier = Modifier.padding(
230261
vertical = dimensionResource(R.dimen.padding_extra_small),
231-
horizontal = dimensionResource(R.dimen.dimen_standard)
262+
horizontal = dimensionResource(R.dimen.dimen_standard),
232263
),
233264
thickness = 1.dp,
234265
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -253,9 +284,11 @@ fun PasskeysDetailsRow(
253284
iconSvgString: String?,
254285
credentialProviderName: String,
255286
passkeyCreationDate: String,
287+
isChecked: Boolean,
288+
modifier: Modifier = Modifier,
256289
) {
257290
Row(
258-
modifier = Modifier
291+
modifier = modifier
259292
.fillMaxWidth()
260293
.padding(vertical = dimensionResource(R.dimen.padding_small)),
261294
verticalAlignment = Alignment.CenterVertically,
@@ -268,6 +301,12 @@ fun PasskeysDetailsRow(
268301
.build(),
269302
)
270303

304+
Checkbox(
305+
checked = isChecked,
306+
onCheckedChange = {},
307+
modifier = Modifier.clickable(enabled = false, onClick = { })
308+
)
309+
271310
Image(
272311
modifier = Modifier.size(48.dp),
273312
painter = painter,
@@ -332,6 +371,8 @@ fun PasskeyManagementScreenPreview() {
332371
)
333372
),
334373
aaguidData = mapOf(),
374+
onItemClick = { _ -> },
375+
onSignal = { },
335376
)
336377
}
337378
}

Shrine/app/src/main/java/com/authentication/shrine/ui/SettingsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ fun SettingsScreen(
171171
if (snackbarMessage != null) {
172172
LaunchedEffect(snackbarMessage) {
173173
snackbarHostState.showSnackbar(
174-
message = snackbarMessage
174+
message = snackbarMessage,
175175
)
176176
}
177177
}

0 commit comments

Comments
 (0)