diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index a6ceb82..bb2684a 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ - + + @@ -51,14 +52,25 @@ + - + + + \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt index ef33d9d..fe551f6 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.spruceid.mobilesdkexample.db.AppDatabase +import com.spruceid.mobilesdkexample.db.VerificationActivityLogsRepository import com.spruceid.mobilesdkexample.db.VerificationMethodsRepository import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.navigation.SetupNavGraph @@ -20,6 +21,9 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorBase1 import com.spruceid.mobilesdkexample.ui.theme.MobileSdkTheme import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModelFactory +import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModelFactory import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModelFactory @@ -71,14 +75,22 @@ class MainActivity : ComponentActivity() { VerificationMethodsViewModelFactory((application as MainApplication).verificationMethodsRepository) } + val verificationActivityLogsViewModel: VerificationActivityLogsViewModel by viewModels { + VerificationActivityLogsViewModelFactory((application as MainApplication).verificationActivityLogsRepository) + } + val credentialPacksViewModel: CredentialPacksViewModel by viewModels { CredentialPacksViewModelFactory(application as MainApplication) } + val helpersViewModel: HelpersViewModel by viewModels() + SetupNavGraph( navController, verificationMethodsViewModel = verificationMethodsViewModel, - credentialPacksViewModel = credentialPacksViewModel + verificationActivityLogsViewModel = verificationActivityLogsViewModel, + credentialPacksViewModel = credentialPacksViewModel, + helpersViewModel = helpersViewModel ) } } @@ -92,4 +104,5 @@ class MainApplication : Application() { // val rawCredentialsRepository by lazy { RawCredentialsRepository(db.rawCredentialsDao()) } val verificationMethodsRepository by lazy { VerificationMethodsRepository(db.verificationMethodsDao()) } + val verificationActivityLogsRepository by lazy { VerificationActivityLogsRepository(db.verificationActivityLogsDao()) } } \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt index c798847..708d79e 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt @@ -14,7 +14,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase RawCredentials::class, VerificationMethods::class ], - version = 3 + version = 4 ) @TypeConverters(*[DateConverter::class]) abstract class AppDatabase : RoomDatabase() { @@ -35,6 +35,7 @@ abstract class AppDatabase : RoomDatabase() { "referenceAppDb", ) .addMigrations(MIGRATION_2_3) + .addMigrations(MIGRATION_3_4) .allowMainThreadQueries() .build() dbInstance = instance @@ -46,13 +47,30 @@ abstract class AppDatabase : RoomDatabase() { val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE `verification_methods` (" + - "`id` INTEGER NOT NULL, " + - "`type` TEXT NOT NULL, " + - "`name` TEXT NOT NULL, " + - "`description` TEXT NOT NULL, " + - "`verifierName` TEXT NOT NULL, " + - "`url` TEXT NOT NULL, " + - "PRIMARY KEY(`id`))") + database.execSQL( + "CREATE TABLE `verification_methods` (" + + "`id` INTEGER NOT NULL, " + + "`type` TEXT NOT NULL, " + + "`name` TEXT NOT NULL, " + + "`description` TEXT NOT NULL, " + + "`verifierName` TEXT NOT NULL, " + + "`url` TEXT NOT NULL, " + + "PRIMARY KEY(`id`))" + ) + } +} + +val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE verification_activity_logs") + database.execSQL( + "CREATE TABLE `verification_activity_logs` (" + + "`id` INTEGER NOT NULL, " + + "`credentialTitle` TEXT NOT NULL, " + + "`issuer` TEXT NOT NULL, " + + "`verificationDateTime` INTEGER NOT NULL, " + + "`additionalInformation` TEXT NOT NULL, " + + "PRIMARY KEY(`id`))" + ) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt index ed0f376..f8c28eb 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt @@ -9,8 +9,18 @@ interface VerificationActivityLogsDao { @Insert suspend fun insertVerificationActivity(verificationActivityLogs: VerificationActivityLogs) - @Query("SELECT * FROM verification_activity_logs") + @Query("SELECT * FROM verification_activity_logs ORDER BY verificationDateTime DESC") fun getAllVerificationActivityLogs(): List + + @Query( + "SELECT * FROM verification_activity_logs " + + "WHERE verificationDateTime > :fromDate " + + "ORDER BY verificationDateTime DESC" + ) + fun getFilteredVerificationActivityLogs(fromDate: Long): List + + @Query("SELECT DISTINCT credentialTitle FROM verification_activity_logs") + fun getDistinctCredentialTitles(): List } @Dao diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt index f6f7b5c..5c14eaf 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt @@ -7,11 +7,10 @@ import java.sql.Date @Entity(tableName = "verification_activity_logs") data class VerificationActivityLogs( @PrimaryKey(autoGenerate = true) val id: Long = 0, - val name: String, val credentialTitle: String, - val date: Date, - val expirationDate: Date, - val status: String, + val issuer: String, + val verificationDateTime: Date, + val additionalInformation: String, ) @Entity(tableName = "raw_credentials") diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt index 4d5161b..4dbc671 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt @@ -1,6 +1,7 @@ package com.spruceid.mobilesdkexample.db import androidx.annotation.WorkerThread +import java.sql.Date class VerificationActivityLogsRepository(private val verificationActivityLogsDao: VerificationActivityLogsDao) { val verificationActivityLogs: List = @@ -15,6 +16,19 @@ class VerificationActivityLogsRepository(private val verificationActivityLogsDao suspend fun getVerificationActivityLogs(): List { return verificationActivityLogsDao.getAllVerificationActivityLogs() } + + // TODO: Add fromDate and credentialType filter params + @WorkerThread + fun getFilteredVerificationActivityLogs(): List { + return verificationActivityLogsDao.getFilteredVerificationActivityLogs( + fromDate = Date(Long.MIN_VALUE).time + ) + } + + @WorkerThread + fun getDistinctCredentialTitles(): List { + return verificationActivityLogsDao.getDistinctCredentialTitles() + } } class RawCredentialsRepository(private val rawCredentialsDao: RawCredentialsDao) { @@ -42,7 +56,8 @@ class RawCredentialsRepository(private val rawCredentialsDao: RawCredentialsDao) } class VerificationMethodsRepository(private val verificationMethodsDao: VerificationMethodsDao) { - val verificationMethods: List = verificationMethodsDao.getAllVerificationMethods() + val verificationMethods: List = + verificationMethodsDao.getAllVerificationMethods() @WorkerThread suspend fun insertVerificationMethod(verificationMethod: VerificationMethods) { diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt index 19eb22a..10268d9 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt @@ -7,6 +7,7 @@ const val VERIFY_VC_PATH = "verify_vc" const val VERIFY_MDOC_PATH = "verify_mdoc" const val VERIFY_DELEGATED_OID4VP_PATH = "verify_delegated_oid4vp/{id}" const val VERIFIER_SETTINGS_HOME_PATH = "verifier_settings_home" +const val VERIFIER_SETTINGS_ACTIVITY_LOG = "verifier_settings_activity_log" const val ADD_VERIFICATION_METHOD_PATH = "add_verification_method" const val WALLET_SETTINGS_HOME_PATH = "wallet_settings_home" const val ADD_TO_WALLET_PATH = "add_to_wallet/{rawCredential}" @@ -22,6 +23,7 @@ sealed class Screen(val route: String) { object VerifyMDocScreen : Screen(VERIFY_MDOC_PATH) object VerifyDelegatedOid4vpScreen : Screen(VERIFY_DELEGATED_OID4VP_PATH) object VerifierSettingsHomeScreen : Screen(VERIFIER_SETTINGS_HOME_PATH) + object VerifierSettingsActivityLogScreen : Screen(VERIFIER_SETTINGS_ACTIVITY_LOG) object AddVerificationMethodScreen : Screen(ADD_VERIFICATION_METHOD_PATH) object WalletSettingsHomeScreen : Screen(WALLET_SETTINGS_HOME_PATH) object AddToWalletScreen : Screen(ADD_TO_WALLET_PATH) diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt index aabc737..96103fd 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt @@ -15,8 +15,11 @@ import com.spruceid.mobilesdkexample.verifier.VerifyDelegatedOid4vpView import com.spruceid.mobilesdkexample.verifier.VerifyEAView import com.spruceid.mobilesdkexample.verifier.VerifyMDocView import com.spruceid.mobilesdkexample.verifier.VerifyVCView +import com.spruceid.mobilesdkexample.verifiersettings.VerifierSettingsActivityLogScreen import com.spruceid.mobilesdkexample.verifiersettings.VerifierSettingsHomeView import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import com.spruceid.mobilesdkexample.wallet.DispatchQRView import com.spruceid.mobilesdkexample.wallet.HandleOID4VCIView @@ -27,7 +30,9 @@ import com.spruceid.mobilesdkexample.walletsettings.WalletSettingsHomeView fun SetupNavGraph( navController: NavHostController, verificationMethodsViewModel: VerificationMethodsViewModel, - credentialPacksViewModel: CredentialPacksViewModel + verificationActivityLogsViewModel: VerificationActivityLogsViewModel, + credentialPacksViewModel: CredentialPacksViewModel, + helpersViewModel: HelpersViewModel ) { NavHost(navController = navController, startDestination = Screen.HomeScreen.route) { composable( @@ -48,16 +53,33 @@ fun SetupNavGraph( } composable( route = Screen.VerifyDLScreen.route, - ) { VerifyDLView(navController) } + ) { + VerifyDLView( + navController, + verificationActivityLogsViewModel = verificationActivityLogsViewModel + ) + } composable( route = Screen.VerifyEAScreen.route, - ) { VerifyEAView(navController) } + ) { + VerifyEAView( + navController, + verificationActivityLogsViewModel = verificationActivityLogsViewModel + ) + } composable( route = Screen.VerifyVCScreen.route, - ) { VerifyVCView(navController) } + ) { + VerifyVCView(navController) + } composable( route = Screen.VerifyMDocScreen.route, - ) { VerifyMDocView(navController) } + ) { + VerifyMDocView( + navController, + verificationActivityLogsViewModel = verificationActivityLogsViewModel + ) + } composable( route = Screen.VerifyDelegatedOid4vpScreen.route, ) { backStackEntry -> @@ -65,7 +87,8 @@ fun SetupNavGraph( VerifyDelegatedOid4vpView( navController, verificationId = id, - verificationMethodsViewModel + verificationMethodsViewModel = verificationMethodsViewModel, + verificationActivityLogsViewModel = verificationActivityLogsViewModel ) } composable( @@ -76,6 +99,15 @@ fun SetupNavGraph( verificationMethodsViewModel = verificationMethodsViewModel ) } + composable( + route = Screen.VerifierSettingsActivityLogScreen.route, + ) { + VerifierSettingsActivityLogScreen( + navController, + verificationActivityLogsViewModel = verificationActivityLogsViewModel, + helpersViewModel = helpersViewModel + ) + } composable( route = Screen.AddVerificationMethodScreen.route, ) { diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/utils/DropdownInput.kt b/example/src/main/java/com/spruceid/mobilesdkexample/utils/DropdownInput.kt new file mode 100644 index 0000000..2a18a9b --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/utils/DropdownInput.kt @@ -0,0 +1,133 @@ +package com.spruceid.mobilesdkexample.utils + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.spruceid.mobilesdkexample.R +import com.spruceid.mobilesdkexample.ui.theme.ColorBase1 +import com.spruceid.mobilesdkexample.ui.theme.ColorBase300 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone500 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone600 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 +import com.spruceid.mobilesdkexample.ui.theme.MobileSdkTheme + +@Composable +fun DropdownInput( + options: List, + onSelect: (String) -> Unit +) { + var selected by remember { mutableStateOf("") } + var expanded by remember { mutableStateOf(false) } + var buttonSize by remember { mutableStateOf(IntSize.Zero) } + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopStart + ) { + // Input field + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + buttonSize = coordinates.size + } + .border( + width = 1.dp, + color = ColorBase300, + shape = RoundedCornerShape(8.dp) + ) + .padding( + start = 16.dp, + end = 10.dp, + top = 10.dp, + bottom = 10.dp + ) + .clickable { + expanded = !expanded + } + ) { + if (selected.isEmpty()) { + Text( + text = "Select...", + color = ColorStone500 + ) + } else { + Text( + text = selected, + color = ColorStone950 + ) + } + Spacer(Modifier.weight(1f)) + Icon( + painter = painterResource(id = R.drawable.chevron), + contentDescription = "Collapse menu button", + tint = ColorStone600, + modifier = Modifier + .rotate(90f) + .height(12.dp) + ) + } + + // Dropdown menu + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .width(with(LocalDensity.current) { + buttonSize.width.toDp() + }) + .background(ColorBase1) + ) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { + selected = option + onSelect(option) + expanded = false + }, + colors = MenuDefaults.itemColors(textColor = ColorStone600), + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun DropdownInputPreview() { + MobileSdkTheme { + DropdownInput( + options = listOf("Option 1", "Option 2", "Option 3"), + onSelect = { } + ) + } +} \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt index b4982dc..e79cbe3 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt @@ -16,10 +16,11 @@ import com.spruceid.mobile.sdk.rs.JwtVc import com.spruceid.mobile.sdk.rs.Mdoc import com.spruceid.mobile.sdk.rs.Uuid import com.spruceid.mobile.sdk.rs.Vcdm2SdJwt -import com.spruceid.mobilesdkexample.credentials.AchievementCredentialItem import com.spruceid.mobilesdkexample.credentials.GenericCredentialItem import com.spruceid.mobilesdkexample.credentials.ICredentialView import org.json.JSONObject +import java.sql.Date +import java.text.SimpleDateFormat const val keyPEM = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEAqKZdZQgPVtjlEB\nfz2ItHG8oXIONenOxRePtqOQ42yhRANCAATA43gI2Ib8+qKK4YEOfNCRiNOhyHaC\nLgAvKdhHS+y6wpG3oJ2xudXagzKKbcfvUda4x0j8zR1/oD56mpm85GbO\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIICgDCCAiWgAwIBAgIUTp04dh8m8Vxa/hX5LmTvjSWrAS8wCgYIKoZIzj0EAwIw\ngZQxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3\nIFlvcmsxEjAQBgNVBAoMCVNwcnVjZSBJRDESMBAGA1UECwwJU3BydWNlIElkMRIw\nEAYDVQQDDAlTcHJ1Y2UgSUQxIzAhBgkqhkiG9w0BCQEWFGNvbnRhY3RAc3BydWNl\naWQuY29tMB4XDTI0MDIxMjE2NTEwMVoXDTI1MDIxMTE2NTEwMVowgZQxCzAJBgNV\nBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxEjAQ\nBgNVBAoMCVNwcnVjZSBJRDESMBAGA1UECwwJU3BydWNlIElkMRIwEAYDVQQDDAlT\ncHJ1Y2UgSUQxIzAhBgkqhkiG9w0BCQEWFGNvbnRhY3RAc3BydWNlaWQuY29tMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwON4CNiG/PqiiuGBDnzQkYjToch2gi4A\nLynYR0vsusKRt6CdsbnV2oMyim3H71HWuMdI/M0df6A+epqZvORmzqNTMFEwHQYD\nVR0OBBYEFPbjKnGAa0aSXw0oe4KfHdN5M1ssMB8GA1UdIwQYMBaAFPbjKnGAa0aS\nXw0oe4KfHdN5M1ssMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIh\nAO2msc7LSdakGcw3q7DxEySqzepr+LeWWNvPbQypQxd8AiEAj7dVI3V00gq3K3OU\nCbkeKnYiGtVCZnXnR/MW91mPeGE=\n-----END CERTIFICATE-----" @@ -29,8 +30,15 @@ const val keyBase64 = val trustedDids = MutableList(1) { "did:web:companion.ler-sandbox.spruceid.xyz:oid4vp:client" } -val delegatedVerifierBaseUrl = "https://credible.ler-sandbox.spruceid.xyz/oid4vp" -val delegatedVerifierUrl = "/api2/verifier/1/delegate" +fun getCurrentSqlDate(): Date { + val currentTimeMillis = System.currentTimeMillis() + return Date(currentTimeMillis) +} + +fun formatSqlDateTime(sqlDate: Date): String { + val formatter = SimpleDateFormat("MMM dd, yyyy 'at' h:mm a") + return formatter.format(sqlDate) +} fun String.splitCamelCase() = replace( String.format( @@ -44,6 +52,7 @@ fun String.splitCamelCase() = replace( fun String.removeUnderscores() = replace("_", "") +fun String.removeCommas() = replace(",", "") fun String.isDate(): Boolean { @@ -112,14 +121,14 @@ fun keyPathFinder(json: Any, path: MutableList): Any { } fun credentialDisplaySelector(rawCredential: String, onDelete: (() -> Unit)?): ICredentialView { -/* This is temporarily commented on until we define the specific AchievementCredentialItem design */ + /* This is temporarily commented on until we define the specific AchievementCredentialItem design */ // try { // Test if it is SdJwt // val credentialPack = CredentialPack() // credentialPack.addSdJwt(Vcdm2SdJwt.newFromCompactSdJwt(rawCredential)) // return AchievementCredentialItem(credentialPack, onDelete) // } catch (_: Exception) { - return GenericCredentialItem(rawCredential, onDelete) + return GenericCredentialItem(rawCredential, onDelete) // } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt index 785cb03..345ce99 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt @@ -33,6 +33,7 @@ import com.spruceid.mobilesdkexample.utils.splitCamelCase fun VerifierCredentialSuccessView( rawCredential: String, onClose: () -> Unit, + logVerification: (String, String) -> Unit ) { val credentialItem = credentialDisplaySelector(rawCredential, null) var title by remember { mutableStateOf(null) } @@ -64,6 +65,8 @@ fun VerifierCredentialSuccessView( issuer = claims?.getJSONObject("issuer")?.getString("name").toString() } catch (_: Exception) { } + + logVerification(title ?: "", issuer ?: "") } Column( diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierMDocResultView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierMDocResultView.kt index 17faa35..1247dcf 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierMDocResultView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierMDocResultView.kt @@ -87,6 +87,7 @@ fun VerifierMDocResultView( val eyeColor = getDiscriminant(result["org.iso.18013.5.1"]?.get("eye_colour")!!) val hairColor = getDiscriminant(result["org.iso.18013.5.1"]?.get("hair_colour")!!) val portrait = result["org.iso.18013.5.1"]?.get("portrait")!! as MDocItem.Array + val issuingAuthority = getDiscriminant(result["org.iso.18013.5.1"]?.get("issuing_authority")!!) val portraitBytes = mDocArrayToByteArray(portrait) Box( @@ -115,7 +116,7 @@ fun VerifierMDocResultView( style = MaterialTheme.typography.headerH2 ) Text( - text = "Issuer", + text = issuingAuthority, color = ColorStone600, style = MaterialTheme.typography.bodyMdDefault ) @@ -328,7 +329,8 @@ fun MDocVerifyPreview() { "eye_colour" to MDocItem.Text("green"), "hair_colour" to MDocItem.Text("unknown"), "resident_address" to MDocItem.Text("2415 1ST AVE, SACRAMENTO 95818"), - "document_number" to MDocItem.Text("I8882610") + "document_number" to MDocItem.Text("I8882610"), + "issuing_authority" to MDocItem.Text("SpruceID") ), "org.iso.18013.5.1.aamva" to mapOf( "DHS_compliance" to MDocItem.Text("F"), diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDLView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDLView.kt index 0646abd..8ceb116 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDLView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDLView.kt @@ -9,9 +9,13 @@ import androidx.compose.runtime.setValue import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.spruceid.mobile.sdk.rs.verifyPdf417Barcode +import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.ScanningComponent import com.spruceid.mobilesdkexample.ScanningType +import com.spruceid.mobilesdkexample.db.VerificationActivityLogs import com.spruceid.mobilesdkexample.navigation.Screen +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -19,19 +23,32 @@ import kotlinx.coroutines.launch @Composable fun VerifyDLView( navController: NavController, + verificationActivityLogsViewModel: VerificationActivityLogsViewModel, ) { - var success by remember { - mutableStateOf(null) - } + var success by remember { mutableStateOf(null) } + var verifying by remember { mutableStateOf(false) } + fun onRead(content: String) { - GlobalScope.launch { - try { - verifyPdf417Barcode(payload = content) - success = true - } catch (e: Exception) { - success = false - e.printStackTrace() + if (!verifying) { + verifying = true + GlobalScope.launch { + try { + verifyPdf417Barcode(payload = content) + success = true + verificationActivityLogsViewModel.saveVerificationActivityLog( + VerificationActivityLogs( + credentialTitle = "Driver's License", + issuer = "Utopia Department of Motor Vehicles", + verificationDateTime = getCurrentSqlDate(), + additionalInformation = "" + ) + ) + } catch (e: Exception) { + success = false + e.printStackTrace() + } + verifying = false } } } @@ -44,7 +61,9 @@ fun VerifyDLView( } } - if (success == null) { + if (verifying) { + LoadingView(loadingText = "Verifying...") + } else if (success == null) { ScanningComponent( subtitle = "Scan the\nback of your driver's license", scanningType = ScanningType.PDF417, diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt index 893aac9..b5881d2 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt @@ -29,12 +29,15 @@ import com.spruceid.mobile.sdk.rs.DelegatedVerifier import com.spruceid.mobile.sdk.rs.DelegatedVerifierStatus import com.spruceid.mobilesdkexample.ErrorView import com.spruceid.mobilesdkexample.LoadingView +import com.spruceid.mobilesdkexample.db.VerificationActivityLogs import com.spruceid.mobilesdkexample.db.VerificationMethods import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.rememberQrBitmapPainter import com.spruceid.mobilesdkexample.ui.theme.ColorStone300 import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import io.ktor.http.Url import kotlinx.coroutines.launch @@ -50,7 +53,8 @@ enum class VerifyDelegatedOid4vpViewSteps { fun VerifyDelegatedOid4vpView( navController: NavController, verificationId: String, - verificationMethodsViewModel: VerificationMethodsViewModel + verificationMethodsViewModel: VerificationMethodsViewModel, + verificationActivityLogsViewModel: VerificationActivityLogsViewModel, ) { val scope = rememberCoroutineScope() @@ -181,6 +185,18 @@ fun VerifyDelegatedOid4vpView( VerifierCredentialSuccessView( rawCredential = presentation!!, onClose = { back() }, + logVerification = { title, issuer -> + scope.launch { + verificationActivityLogsViewModel.saveVerificationActivityLog( + VerificationActivityLogs( + credentialTitle = title, + issuer = issuer, + verificationDateTime = getCurrentSqlDate(), + additionalInformation = "" + ) + ) + } + } ) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyEAView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyEAView.kt index da35ae7..6c55ac8 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyEAView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyEAView.kt @@ -13,7 +13,10 @@ import com.spruceid.mobile.sdk.rs.verifyVcbQrcodeAgainstMrz import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.ScanningComponent import com.spruceid.mobilesdkexample.ScanningType +import com.spruceid.mobilesdkexample.db.VerificationActivityLogs import com.spruceid.mobilesdkexample.navigation.Screen +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -24,19 +27,13 @@ enum class VerifyEASteps { @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun VerifyEAView( - navController: NavController + navController: NavController, + verificationActivityLogsViewModel: VerificationActivityLogsViewModel, ) { - var step by remember { - mutableStateOf(VerifyEASteps.STEP_ONE) - } - - var success by remember { - mutableStateOf(null) - } - - var stepOne by remember { - mutableStateOf(null) - } + var step by remember { mutableStateOf(VerifyEASteps.STEP_ONE) } + var success by remember { mutableStateOf(null) } + var stepOne by remember { mutableStateOf(null) } + var verifying by remember { mutableStateOf(false) } fun onReadStepOne(content: String) { stepOne = content @@ -47,16 +44,27 @@ fun VerifyEAView( } fun onReadStepTwo(content: String) { - success = true - GlobalScope.launch { - try { - verifyVcbQrcodeAgainstMrz(mrzPayload = content, qrPayload = stepOne!!) - success = true - } catch (e: Exception) { - success = false - e.printStackTrace() + if (!verifying) { + verifying = true + GlobalScope.launch { + try { + verifyVcbQrcodeAgainstMrz(mrzPayload = content, qrPayload = stepOne!!) + success = true + verificationActivityLogsViewModel.saveVerificationActivityLog( + VerificationActivityLogs( + credentialTitle = "Employment Authorization", + issuer = "State of Utopia", + verificationDateTime = getCurrentSqlDate(), + additionalInformation = "" + ) + ) + } catch (e: Exception) { + success = false + e.printStackTrace() + } + step = VerifyEASteps.SUCCESS + verifying = false } - step = VerifyEASteps.SUCCESS } } @@ -68,38 +76,42 @@ fun VerifyEAView( } } - when (step) { - VerifyEASteps.STEP_ONE -> { - ScanningComponent( - subtitle = "Scan the front of your\nemployment authorization", - scanningType = ScanningType.QRCODE, - onRead = ::onReadStepOne, - onCancel = ::back - ) - } + if (verifying) { + LoadingView(loadingText = "Verifying...") + } else { + when (step) { + VerifyEASteps.STEP_ONE -> { + ScanningComponent( + subtitle = "Scan the front of your\nemployment authorization", + scanningType = ScanningType.QRCODE, + onRead = ::onReadStepOne, + onCancel = ::back + ) + } - VerifyEASteps.INTERMEDIATE -> { - LoadingView( - loadingText = "Verifying..." - ) - } + VerifyEASteps.INTERMEDIATE -> { + LoadingView( + loadingText = "Verifying..." + ) + } - VerifyEASteps.STEP_TWO -> { - ScanningComponent( - title = "Scan MRZ", - subtitle = "Scan the back of your document", - scanningType = ScanningType.MRZ, - onRead = ::onReadStepTwo, - onCancel = ::back - ) - } + VerifyEASteps.STEP_TWO -> { + ScanningComponent( + title = "Scan MRZ", + subtitle = "Scan the back of your document", + scanningType = ScanningType.MRZ, + onRead = ::onReadStepTwo, + onCancel = ::back + ) + } - VerifyEASteps.SUCCESS -> { - VerifierBinarySuccessView( - success = success!!, - description = if (success!!) "Valid Employment Authorization" else "Invalid Employment Authorization", - onClose = ::back - ) + VerifyEASteps.SUCCESS -> { + VerifierBinarySuccessView( + success = success!!, + description = if (success!!) "Valid Employment Authorization" else "Invalid Employment Authorization", + onClose = ::back + ) + } } } } \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyMDocView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyMDocView.kt index ead3dcf..0598986 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyMDocView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyMDocView.kt @@ -30,8 +30,11 @@ import com.spruceid.mobile.sdk.rs.MDocItem import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.ScanningComponent import com.spruceid.mobilesdkexample.ScanningType +import com.spruceid.mobilesdkexample.db.VerificationActivityLogs import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.utils.checkAndRequestBluetoothPermissions +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -85,7 +88,10 @@ enum class State { ExperimentalPermissionsApi::class ) @Composable -fun VerifyMDocView(navController: NavController) { +fun VerifyMDocView( + navController: NavController, + verificationActivityLogsViewModel: VerificationActivityLogsViewModel, +) { val context = LocalContext.current var reader: IsoMdlReader? = null @@ -116,6 +122,14 @@ fun VerifyMDocView(navController: NavController) { if (state.containsKey("mdl")) { result = reader?.handleResponse(state["mdl"] as ByteArray) scanProcessState = State.DONE + VerificationActivityLogs( + credentialTitle = "Driver's License", + issuer = getDiscriminant( + result!!["org.iso.18013.5.1"]?.get("issuing_authority")!! + ), + verificationDateTime = getCurrentSqlDate(), + additionalInformation = "" + ) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerificationActivityLogScreen.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerificationActivityLogScreen.kt deleted file mode 100644 index cef8f36..0000000 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerificationActivityLogScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.spruceid.mobilesdkexample.verifiersettings - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.spruceid.mobilesdkexample.db.VerificationActivityLogs -import com.spruceid.mobilesdkexample.ui.theme.ColorBlue600 -import com.spruceid.mobilesdkexample.ui.theme.ColorStone300 -import com.spruceid.mobilesdkexample.ui.theme.ColorStone600 -import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 -import com.spruceid.mobilesdkexample.ui.theme.Inter -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -@Composable -fun VerificationActivityLogsScreen() { -// val verificationActivityLogs by verificationActivityLogsViewModel.verificationActivityLogs.collectAsState() - val verificationActivityLogs = listOf() - val dateFormatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()) - - LazyColumn( - Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp), - ) { - item { - Text( - text = "Coming Soon", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - fontSize = 17.sp, - color = ColorStone950, - modifier = Modifier.padding(bottom = 4.dp), - ) - } - items(verificationActivityLogs) { log -> - Column { - Text( - text = log.name, - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - fontSize = 17.sp, - color = ColorStone950, - modifier = Modifier.padding(bottom = 4.dp), - ) - Text( - text = log.credentialTitle, - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = ColorStone600, - modifier = Modifier.padding(bottom = 4.dp), - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = - Modifier - .fillMaxWidth(), - ) { - Text( - text = log.status, - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = ColorStone600, - modifier = - Modifier - .padding(bottom = 4.dp), - ) - Text( - text = "${ - if (log.expirationDate.before( - Date(), - ) - ) { - "expired" - } else { - "expires" - } - } on ${dateFormatter.format(log.expirationDate)}", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = ColorStone600, - modifier = Modifier.padding(bottom = 4.dp), - ) - } - Text( - text = "Scanned on ${dateFormatter.format(log.date)}", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - fontStyle = FontStyle.Italic, - textAlign = TextAlign.End, - color = ColorStone300, - modifier = Modifier.padding(top = 8.dp, bottom = 4.dp), - ) - HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp)) - } - } - item { - Button( - onClick = { -// settingsViewModel.exportMetrics(logsViewModel.generateActivityLogCSV(), "activity_logs.csv") - }, - modifier = - Modifier - .fillMaxWidth() - .padding(20.dp), - colors = - ButtonDefaults.buttonColors( - containerColor = ColorBlue600, - contentColor = Color.White, - ), - ) { - Text( - text = "Export", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - color = Color.White, - ) - } - } - } -} \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsActivityLogScreen.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsActivityLogScreen.kt new file mode 100644 index 0000000..185c511 --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsActivityLogScreen.kt @@ -0,0 +1,367 @@ +package com.spruceid.mobilesdkexample.verifiersettings + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.navigation.NavController +import com.spruceid.mobilesdkexample.R +import com.spruceid.mobilesdkexample.db.VerificationActivityLogs +import com.spruceid.mobilesdkexample.ui.theme.ColorBase1 +import com.spruceid.mobilesdkexample.ui.theme.ColorBase50 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone300 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone400 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone600 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone700 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone900 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 +import com.spruceid.mobilesdkexample.ui.theme.Inter +import com.spruceid.mobilesdkexample.ui.theme.MobileSdkTheme +import com.spruceid.mobilesdkexample.utils.DropdownInput +import com.spruceid.mobilesdkexample.utils.formatSqlDateTime +import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel +import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel + +@Composable +fun VerifierSettingsActivityLogScreen( + navController: NavController, + verificationActivityLogsViewModel: VerificationActivityLogsViewModel, + helpersViewModel: HelpersViewModel +) { + // TODO: WIP: we will finish these filters in the future + // val distinctCredentialTitles = verificationActivityLogsViewModel.getDistinctCredentialTitles() + val verificationActivityLogs by verificationActivityLogsViewModel.verificationActivityLogs.collectAsState() + + Column( + Modifier + .padding(all = 20.dp) + .padding(top = 20.dp) + ) { + VerifierSettingsActivityLogScreenHeader( + onBack = { + navController.popBackStack() + } + ) + VerifierSettingsActivityLogScreenBody( + verificationActivityLogs = verificationActivityLogs, + export = { logs -> + helpersViewModel.exportCSV( + verificationActivityLogsViewModel.generateVerificationActivityLogCSV(logs = logs), + "activity_logs.csv" + ) + } + ) + // TODO: WIP: we will finish these filters in the future + // FilterModal() + } +} + +@Composable +fun VerifierSettingsActivityLogScreenHeader(onBack: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(36.dp) + .clickable { + onBack() + } + ) { + Image( + painter = painterResource(id = R.drawable.chevron), + contentDescription = stringResource(id = R.string.chevron), + modifier = Modifier + .rotate(180f) + .scale(0.4f) + ) + Text( + text = "Activity Log", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + color = ColorStone950 + ) + Spacer(Modifier.weight(1f)) + + // TODO: WIP: we will finish these filters in the future + // val distinctCredentialTitles = verificationActivityLogsViewModel.getDistinctCredentialTitles() +// Box( +// contentAlignment = Alignment.Center, +// modifier = Modifier +// .width(36.dp) +// .height(36.dp) +// .padding(start = 4.dp) +// .shadow( +// elevation = 10.dp, +// spotColor = ColorStone950, +// shape = RoundedCornerShape(6.dp) +// ) +// .border( +// width = 1.dp, +// color = ColorBase100, +// shape = RoundedCornerShape(6.dp) +// ) +// .background(ColorBase1) +// .clickable { +// +// } +// ) { +// Image( +// painter = painterResource(id = R.drawable.filter), +// contentDescription = stringResource(id = R.string.filter), +// modifier = Modifier +// .width(20.dp) +// .height(20.dp) +// ) +// } + } +} + +@Composable +fun VerifierSettingsActivityLogScreenBody( + verificationActivityLogs: List, + export: (List) -> Unit +) { + Column( + Modifier + .padding(top = 10.dp) + .navigationBarsPadding(), + ) { + if (verificationActivityLogs.isEmpty()) { + Column( + Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "No Activity Log Found", + fontFamily = Inter, + fontSize = 20.sp, + fontWeight = FontWeight.Normal, + color = ColorStone400 + ) + } + } else { + LazyColumn( + Modifier + .padding(top = 10.dp) + .fillMaxSize() + .weight(weight = 1f, fill = false), + ) { + items(verificationActivityLogs) { log -> + Column { + Text( + text = log.credentialTitle, + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + color = ColorStone950, + modifier = Modifier.padding(bottom = 4.dp), + ) + Text( + text = log.issuer, + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorStone600, + modifier = Modifier.padding(bottom = 4.dp), + ) + Text( + text = formatSqlDateTime(log.verificationDateTime), + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorStone600, + modifier = Modifier.padding(bottom = 4.dp), + ) + HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp)) + } + } + } + Button( + onClick = { + export(verificationActivityLogs) + }, + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = ColorStone300, + shape = RoundedCornerShape(100.dp) + ), + colors = ButtonDefaults.buttonColors( + containerColor = ColorBase1, + contentColor = ColorStone950, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.export), + contentDescription = stringResource(id = R.string.export), + modifier = Modifier.padding(end = 5.dp), + ) + Text( + text = "Export", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + color = ColorStone950, + ) + } + } + } + } +} + +// TODO: WIP: We will finish these filters in the future +@Composable +fun FilterModal() { + Dialog(onDismissRequest = { }) { + Surface( + shape = RoundedCornerShape(8.dp), + color = ColorBase1, + tonalElevation = 8.dp, + modifier = Modifier.fillMaxWidth() + ) { + Column { + // Title + Column( + Modifier + .padding(horizontal = 24.dp) + .padding(vertical = 12.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + "Filters", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + color = ColorStone900 + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + modifier = Modifier.clickable { } + ) + } + } + + // Title divider + HorizontalDivider() + + // Body + Column( + Modifier + .padding(horizontal = 24.dp) + .padding(vertical = 12.dp) + ) { + DropdownInput( + options = listOf("Option 1", "Option 2", "Option 3"), + onSelect = { option -> + + } + ) + } + + // Footer divider + HorizontalDivider() + + Row( + Modifier + .padding(horizontal = 14.dp) + .padding(vertical = 12.dp) + ) { + Button( + onClick = { + + }, + shape = RoundedCornerShape(100.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = Color.Black, + ), + border = BorderStroke(1.dp, ColorStone300), + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(end = 6.dp) + ) { + Text( + text = "Cancel", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = Color.Black, + ) + } + Button( + onClick = { + + }, + shape = RoundedCornerShape(100.dp), + colors = ButtonDefaults.buttonColors( + containerColor = ColorStone700, + contentColor = ColorBase50, + ), + border = BorderStroke(1.dp, ColorStone700), + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(start = 6.dp) + ) { + Text( + text = "Apply", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorBase50, + ) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun FilterModalPreview() { + MobileSdkTheme { + FilterModal() + } +} \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsHomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsHomeView.kt index a819719..c1be24b 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsHomeView.kt @@ -1,6 +1,7 @@ package com.spruceid.mobilesdkexample.verifiersettings import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -8,24 +9,21 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.List import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -35,6 +33,7 @@ import androidx.navigation.NavController import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.ui.theme.ColorRose600 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone50 import com.spruceid.mobilesdkexample.ui.theme.ColorStone600 import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter @@ -42,20 +41,11 @@ import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -enum class VerifierSubSettings { - VERIFICATION_ACTIVITY_LOG, -} - @Composable fun VerifierSettingsHomeView( navController: NavController, verificationMethodsViewModel: VerificationMethodsViewModel ) { - - var subpage by remember { - mutableStateOf(null) - } - Column( Modifier .padding(all = 20.dp) @@ -63,142 +53,134 @@ fun VerifierSettingsHomeView( ) { VerifierSettingsHomeHeader( onBack = { - if (subpage != null) { - subpage = null - } else { - navController.navigate( - Screen.HomeScreen.route.replace("{tab}", "verifier") - ) { - popUpTo(0) - } + navController.navigate( + Screen.HomeScreen.route.replace("{tab}", "verifier") + ) { + popUpTo(0) } } ) VerifierSettingsHomeBody( - subpage = subpage, + navController = navController, verificationMethodsViewModel = verificationMethodsViewModel, - changeSubPage = { sp -> - subpage = sp - } ) } } @Composable -fun VerifierSettingsHomeHeader( - onBack: () -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { - onBack() - } - ) { - Image( - painter = painterResource(id = R.drawable.chevron), - contentDescription = stringResource(id = R.string.chevron), - modifier = Modifier - .rotate(180f) - .scale(0.7f) - ) +fun VerifierSettingsHomeHeader(onBack: () -> Unit) { + Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "Verifier Settings", + text = "Settings", fontFamily = Inter, fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - color = ColorStone950, - modifier = Modifier.padding(start = 10.dp) + fontSize = 20.sp, + color = ColorStone950 ) Spacer(Modifier.weight(1f)) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(36.dp) + .height(36.dp) + .padding(start = 4.dp) + .clip(shape = RoundedCornerShape(8.dp)) + .background(ColorStone950) + .clickable { + onBack() + } + ) { + Image( + painter = painterResource(id = R.drawable.cog), + contentDescription = stringResource(id = R.string.cog), + colorFilter = ColorFilter.tint(ColorStone50), + modifier = Modifier + .width(20.dp) + .height(20.dp) + ) + } } - } @Composable fun VerifierSettingsHomeBody( - subpage: VerifierSubSettings?, + navController: NavController, verificationMethodsViewModel: VerificationMethodsViewModel, - changeSubPage: (VerifierSubSettings?) -> Unit ) { - if (subpage == null) { - Column( + Column( + Modifier + .padding(top = 10.dp) + .navigationBarsPadding(), + ) { + Box( Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp) - .navigationBarsPadding(), + .fillMaxWidth() + .clickable { + navController.navigate(Screen.VerifierSettingsActivityLogScreen.route) + }, ) { - Box( - Modifier - .fillMaxWidth() - .clickable { - changeSubPage(VerifierSubSettings.VERIFICATION_ACTIVITY_LOG) - }, - ) { - Column { + Column { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - Icons.AutoMirrored.Outlined.List, - contentDescription = stringResource(id = R.string.verification_activity_log), - modifier = Modifier.padding(end = 5.dp), - ) - Text( - text = "Verification Activity Log", - fontFamily = Inter, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = ColorStone600, - modifier = Modifier.padding(bottom = 5.dp, top = 5.dp), - ) - } - Image( - painter = painterResource(id = R.drawable.chevron), - contentDescription = stringResource(id = R.string.chevron), - modifier = Modifier.scale(0.5f) + painter = painterResource(id = R.drawable.verification_activity_log), + contentDescription = stringResource(id = R.string.verification_activity_log), + modifier = Modifier.padding(end = 5.dp), + ) + Text( + text = "Activity Log", + fontFamily = Inter, + fontWeight = FontWeight.Medium, + fontSize = 17.sp, + color = ColorStone950, + modifier = Modifier.padding(bottom = 5.dp, top = 5.dp), ) } - Text( - text = "view and export verification history", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - color = ColorStone600, + Image( + painter = painterResource(id = R.drawable.chevron), + contentDescription = stringResource(id = R.string.chevron), + modifier = Modifier.scale(0.5f) ) } - } - Spacer(Modifier.weight(1f)) - Button( - onClick = { - GlobalScope.launch { - verificationMethodsViewModel.deleteAllVerificationMethods() - } - }, - shape = RoundedCornerShape(5.dp), - colors = ButtonDefaults.buttonColors( - containerColor = ColorRose600, - contentColor = Color.White, - ), - modifier = Modifier - .fillMaxWidth() - .padding(top = 30.dp) - ) { + Text( - text = "Delete all added verification methods", + text = "View and export verification history", fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = Color.White, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorStone600, ) } } - } else if (subpage == VerifierSubSettings.VERIFICATION_ACTIVITY_LOG) { - VerificationActivityLogsScreen() + Spacer(Modifier.weight(1f)) + Button( + onClick = { + GlobalScope.launch { + verificationMethodsViewModel.deleteAllVerificationMethods() + } + }, + shape = RoundedCornerShape(5.dp), + colors = ButtonDefaults.buttonColors( + containerColor = ColorRose600, + contentColor = Color.White, + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp) + ) { + Text( + text = "Delete all added verification methods", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = Color.White, + ) + } } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/HelpersViewModel.kt b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/HelpersViewModel.kt new file mode 100644 index 0000000..5e29c48 --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/HelpersViewModel.kt @@ -0,0 +1,44 @@ +package com.spruceid.mobilesdkexample.viewmodels + +import android.app.Application +import android.content.Intent +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.lifecycle.AndroidViewModel +import java.io.File +import java.io.PrintWriter + +fun File.clearText() { + PrintWriter(this).also { + it.print("") + it.close() + } +} + +fun File.updateText(content: String) { + clearText() + appendText(content) +} + +class HelpersViewModel(application: Application) : AndroidViewModel(application) { + fun exportCSV(content: String, filename: String) { + val app = getApplication() + val file = File(app.cacheDir, filename) + file.updateText(content) + + val uri = + FileProvider.getUriForFile( + app.baseContext, + app.baseContext.packageName + ".provider", + file, + ) + Intent(Intent.ACTION_SEND).apply { + type = "text/csv" + flags = Intent.FLAG_ACTIVITY_NEW_TASK + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + putExtra(Intent.EXTRA_STREAM, uri) + }.also { intent -> + ContextCompat.startActivity(app.baseContext, intent, null) + } + } +} \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/VerifierActivityLogsViewModel.kt b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/VerifierActivityLogsViewModel.kt index 195aec5..51824e9 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/VerifierActivityLogsViewModel.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/VerifierActivityLogsViewModel.kt @@ -5,11 +5,13 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.spruceid.mobilesdkexample.db.VerificationActivityLogs import com.spruceid.mobilesdkexample.db.VerificationActivityLogsRepository +import com.spruceid.mobilesdkexample.utils.formatSqlDateTime +import com.spruceid.mobilesdkexample.utils.removeCommas import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -class LogsViewModel(private val verificationActivityLogsRepository: VerificationActivityLogsRepository) : +class VerificationActivityLogsViewModel(private val verificationActivityLogsRepository: VerificationActivityLogsRepository) : ViewModel() { private val _verificationActivityLogs = MutableStateFlow(listOf()) val verificationActivityLogs = _verificationActivityLogs.asStateFlow() @@ -27,17 +29,41 @@ class LogsViewModel(private val verificationActivityLogsRepository: Verification verificationActivityLogsRepository.getVerificationActivityLogs() } - fun generateVerificationActivityLogCSV(): String { - val heading = "ID, Full Name, Credential Title, Permit Expiration, Status, Date\n" - return heading + - verificationActivityLogs.value.joinToString("\n") { - "${it.id}, ${it.name}, ${it.credentialTitle}, ${it.expirationDate}, ${it.status}, ${it.date}" - } + // TODO: Add fromDate and credentialType filter params + fun getFilteredVerificationActivityLog() { + verificationActivityLogsRepository.getFilteredVerificationActivityLogs() + } + + fun getDistinctCredentialTitles(): List { + return verificationActivityLogsRepository.getDistinctCredentialTitles() + } + + fun generateVerificationActivityLogCSV(logs: List? = null): String { + val heading = + "ID, Credential Title, Issuer, Verification Date Time, Additional Information\n" + + val rows = logs?.joinToString("\n") { + "${it.id}, " + + "${it.credentialTitle}, " + + "${it.issuer}, " + + "${formatSqlDateTime(it.verificationDateTime).removeCommas()}, " + + it.additionalInformation + } + ?: verificationActivityLogs.value.joinToString("\n") { + "${it.id}, " + + "${it.credentialTitle}, " + + "${it.issuer}, " + + "${formatSqlDateTime(it.verificationDateTime).removeCommas()}, " + + it.additionalInformation + } + + return heading + rows } } -class LogsViewModelFactory(private val repository: VerificationActivityLogsRepository) : +class VerificationActivityLogsViewModelFactory(private val repository: VerificationActivityLogsRepository) : ViewModelProvider.NewInstanceFactory() { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = LogsViewModel(repository) as T + override fun create(modelClass: Class): T = + VerificationActivityLogsViewModel(repository) as T } diff --git a/example/src/main/res/drawable/export.xml b/example/src/main/res/drawable/export.xml new file mode 100644 index 0000000..2b3baba --- /dev/null +++ b/example/src/main/res/drawable/export.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/example/src/main/res/drawable/filter.xml b/example/src/main/res/drawable/filter.xml new file mode 100644 index 0000000..862378b --- /dev/null +++ b/example/src/main/res/drawable/filter.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/example/src/main/res/drawable/verification_activity_log.xml b/example/src/main/res/drawable/verification_activity_log.xml new file mode 100644 index 0000000..32a03f6 --- /dev/null +++ b/example/src/main/res/drawable/verification_activity_log.xml @@ -0,0 +1,9 @@ + + + diff --git a/example/src/main/res/values/strings.xml b/example/src/main/res/values/strings.xml index a73d65b..15b373e 100644 --- a/example/src/main/res/values/strings.xml +++ b/example/src/main/res/values/strings.xml @@ -22,4 +22,6 @@ Start flow Restart Settings + Click to filter + Export \ No newline at end of file diff --git a/example/src/main/res/xml/provider_paths.xml b/example/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..f742886 --- /dev/null +++ b/example/src/main/res/xml/provider_paths.xml @@ -0,0 +1,12 @@ + + + + + +