-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
identity-appsupport: Add QR code generation and scanning composables.
This adds Compose Multiplatform support for QR code generation and scanning. A future change will start using this in the wallet app, currently it's only used in samples/testapp. This adds dependencies on https://github.com/alexzhirkevich/qrose and https://github.com/kalinjul/EasyQRScan which are under the MIT and Apache 2.0 license, respectively. These depencies are wholly hidden behind the `ScanQrCodeDialog` and `ShowQrCodeDialog` in case we want to swap these out in the future. For example, for the scanning part we likely want to just use MLKit directly. Test: New screens in samples/testapp for testing. Tested on Android and iOS. Signed-off-by: David Zeuthen <[email protected]>
- Loading branch information
Showing
11 changed files
with
282 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
...pport/src/commonMain/kotlin/com/android/identity/appsupport/ui/qrcode/ScanQrCodeDialog.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package com.android.identity.appsupport.ui.qrcode | ||
|
||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.material3.AlertDialog | ||
import androidx.compose.material3.Text | ||
import androidx.compose.material3.TextButton | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.unit.dp | ||
import org.publicvalue.multiplatform.qrcode.CodeType | ||
import org.publicvalue.multiplatform.qrcode.ScannerWithPermissions | ||
|
||
/** | ||
* Shows a dialog for scanning QR codes. | ||
* | ||
* If the application doesn't have the necessary permission, the user is prompted to grant it. | ||
* | ||
* @param title The title of the dialog. | ||
* @param description The description text to include in the dialog. | ||
* @param dismissButton The text for the dismiss button. | ||
* @param onCodeScanned called when a QR code is scanned, the parameter is the parsed data. Should | ||
* return `true` to stop scanning, `false` to continue scanning. | ||
* @param onDismiss called when the dismiss button is pressed. | ||
* @param modifier A [Modifier] or `null`. | ||
*/ | ||
@Composable | ||
fun ScanQrCodeDialog( | ||
title: String, | ||
description: String, | ||
dismissButton: String, | ||
onCodeScanned: (data: String) -> Boolean, | ||
onDismiss: () -> Unit, | ||
modifier: Modifier? = null | ||
) { | ||
AlertDialog( | ||
modifier = modifier ?: Modifier, | ||
title = { Text(text = title) }, | ||
text = { | ||
Column( | ||
verticalArrangement = Arrangement.spacedBy(16.dp), | ||
) { | ||
Text(text = description) | ||
|
||
ScannerWithPermissions( | ||
modifier = Modifier.height(300.dp), | ||
onScanned = { data -> | ||
onCodeScanned(data) | ||
}, | ||
types = listOf(CodeType.QR) | ||
) | ||
} | ||
}, | ||
onDismissRequest = { onDismiss.invoke() }, | ||
confirmButton = {}, | ||
dismissButton = { | ||
TextButton( | ||
onClick = { | ||
onDismiss.invoke() | ||
} | ||
) { | ||
Text(dismissButton) | ||
} | ||
} | ||
) | ||
} |
88 changes: 88 additions & 0 deletions
88
...pport/src/commonMain/kotlin/com/android/identity/appsupport/ui/qrcode/ShowQrCodeDialog.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package com.android.identity.appsupport.ui.qrcode | ||
|
||
import androidx.compose.foundation.Image | ||
import androidx.compose.foundation.background | ||
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.layout.size | ||
import androidx.compose.foundation.shape.RoundedCornerShape | ||
import androidx.compose.material3.AlertDialog | ||
import androidx.compose.material3.Text | ||
import androidx.compose.material3.TextButton | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.unit.dp | ||
import io.github.alexzhirkevich.qrose.rememberQrCodePainter | ||
|
||
/** | ||
* Renders a QR code and shows it in a dialog. | ||
* | ||
* @param title The title of the dialog. | ||
* @param description The description text to include in the dialog. | ||
* @param dismissButton The text for the dismiss button. | ||
* @param data the QR code to show, e.g. mdoc:owBjMS4... or https://github.com/.... | ||
* @param onDismiss called when the dismiss button is pressed. | ||
* @param modifier A [Modifier] or `null`. | ||
*/ | ||
@Composable | ||
fun ShowQrCodeDialog( | ||
title: String, | ||
description: String, | ||
dismissButton: String, | ||
data: String, | ||
onDismiss: () -> Unit, | ||
modifier: Modifier? = null | ||
) { | ||
val painter = rememberQrCodePainter( | ||
data = data, | ||
) | ||
|
||
AlertDialog( | ||
modifier = modifier ?: Modifier, | ||
title = { Text(text = title) }, | ||
text = { | ||
Column( | ||
verticalArrangement = Arrangement.spacedBy(16.dp) | ||
) { | ||
Text(text = description) | ||
|
||
Row( | ||
modifier = Modifier.align(Alignment.CenterHorizontally) | ||
) { | ||
Column( | ||
horizontalAlignment = Alignment.CenterHorizontally, | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.clip(shape = RoundedCornerShape(16.dp)) | ||
.background(Color.White) | ||
) { | ||
Image( | ||
painter = painter, | ||
contentDescription = "TODO", | ||
modifier = Modifier | ||
.size(300.dp) | ||
.padding(16.dp) | ||
) | ||
} | ||
} | ||
} | ||
}, | ||
onDismissRequest = { onDismiss.invoke() }, | ||
confirmButton = {}, | ||
dismissButton = { | ||
TextButton( | ||
onClick = { | ||
onDismiss.invoke() | ||
} | ||
) { | ||
Text(dismissButton) | ||
} | ||
} | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/QrCodesScreen.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package com.android.identity.testapp.ui | ||
|
||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.lazy.LazyColumn | ||
import androidx.compose.material3.Text | ||
import androidx.compose.material3.TextButton | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.unit.dp | ||
import com.android.identity.appsupport.ui.qrcode.ShowQrCodeDialog | ||
import com.android.identity.appsupport.ui.qrcode.ScanQrCodeDialog | ||
|
||
@Composable | ||
fun QrCodesScreen( | ||
showToast: (message: String) -> Unit | ||
) { | ||
val showMdocQrCodeDialog = remember { mutableStateOf(false) } | ||
val showUrlQrCodeDialog = remember { mutableStateOf(false) } | ||
val showQrScanDialog = remember { mutableStateOf(false) } | ||
|
||
if (showMdocQrCodeDialog.value) { | ||
ShowQrCodeDialog( | ||
title = "Scan code on reader", | ||
description = "Your personal information won't be shared yet. You don't need to hand your phone to anyone to share your ID.", | ||
dismissButton = "Close", | ||
data = "mdoc:owBjMS4wAYIB2BhYS6QBAiABIVggWojRgrzl9C76WZQ/MzWdAuipaP8onZPl" + | ||
"+kRLYkNDFn/iJYILFujPhY3cdpBAe6YdTDOCNwqM/PPeaqZy/GClV6oy/GcCgYMCAaMA9AH1C1BF7+90KyxIN6kKOw4dBaaRBw==", | ||
onDismiss = { showMdocQrCodeDialog.value = false } | ||
) | ||
} | ||
|
||
if (showUrlQrCodeDialog.value) { | ||
ShowQrCodeDialog( | ||
title = "Scan code with phone", | ||
description = "This is a QR code for https://github.com/openwallet-foundation-labs/identity-credential", | ||
dismissButton = "Close", | ||
data = "https://github.com/openwallet-foundation-labs/identity-credential", | ||
onDismiss = { showUrlQrCodeDialog.value = false } | ||
) | ||
} | ||
|
||
if (showQrScanDialog.value) { | ||
ScanQrCodeDialog( | ||
title = "Scan code", | ||
description = "Ask the person you wish to request identity attributes from to present" + | ||
" a QR code. This is usually in their identity wallet.", | ||
dismissButton = "Close", | ||
onCodeScanned = { data -> | ||
if (data.startsWith("mdoc:")) { | ||
showToast("Scanned mdoc URI $data") | ||
showQrScanDialog.value = false | ||
true | ||
} else { | ||
false | ||
} | ||
}, | ||
onDismiss = { showQrScanDialog.value = false } | ||
) | ||
} | ||
|
||
LazyColumn( | ||
modifier = Modifier.padding(8.dp) | ||
) { | ||
item { | ||
TextButton( | ||
onClick = { showMdocQrCodeDialog.value = true }, | ||
content = { Text("Show mdoc QR code") } | ||
) | ||
} | ||
|
||
item { | ||
TextButton( | ||
onClick = { showUrlQrCodeDialog.value = true }, | ||
content = { Text("Show URL QR code") } | ||
) | ||
} | ||
|
||
item { | ||
TextButton( | ||
onClick = { showQrScanDialog.value = true }, | ||
content = { Text("Scan mdoc QR code") } | ||
) | ||
} | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters