Skip to content

Commit 941db22

Browse files
committed
fix(WAL-995): harden durable issuance sessions
1 parent faa0cd2 commit 941db22

42 files changed

Lines changed: 1572 additions & 263 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/commonMain/kotlin/id/walt/walletdemo/compose/logic/DemoWallet.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ interface DemoWallet {
2323
): WalletDemoIssuanceOutcome = continuePreAuthorizedIssuance(sessionId, null)
2424

2525
suspend fun cancelIssuance(sessionId: String): WalletDemoIssuanceOutcome = WalletDemoIssuanceOutcome.Cancelled
26+
suspend fun resumeDeferredIssuance(deferredCredentialId: String): WalletDemoIssuanceOutcome =
27+
WalletDemoIssuanceOutcome.Failed("Deferred issuance is unavailable")
2628
suspend fun present(requestUrl: String, did: String? = null): WalletDemoOperationResult
2729
suspend fun previewPresentation(requestUrl: String): WalletDemoPresentationPreview
2830
suspend fun submitPresentation(

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/commonMain/kotlin/id/walt/walletdemo/compose/logic/LazyDemoWallet.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ internal class LazyDemoWallet(
3737

3838
override suspend fun cancelIssuance(sessionId: String) = wallet().cancelIssuance(sessionId)
3939

40+
override suspend fun resumeDeferredIssuance(deferredCredentialId: String) =
41+
wallet().resumeDeferredIssuance(deferredCredentialId)
42+
4043
override suspend fun present(requestUrl: String, did: String?): WalletDemoOperationResult =
4144
wallet().present(requestUrl, did)
4245

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/commonMain/kotlin/id/walt/walletdemo/compose/logic/WalletDemoController.kt

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class WalletDemoController(
7474

7575
fun lock() {
7676
receiveJob?.cancel()
77-
issuanceSession = null
77+
discardCurrentIssuance()
7878
_state.update {
7979
it.copy(
8080
auth = WalletAuthState.Login(),
@@ -93,7 +93,7 @@ class WalletDemoController(
9393

9494
fun updateOfferUrl(value: String) {
9595
receiveJob?.cancel()
96-
issuanceSession = null
96+
discardCurrentIssuance()
9797
_state.update {
9898
it.copy(
9999
requestDrafts = it.requestDrafts.copy(
@@ -132,7 +132,7 @@ class WalletDemoController(
132132
when (WalletDeepLinkScheme.parse(url)) {
133133
WalletDeepLinkScheme.CredentialOffer -> {
134134
receiveJob?.cancel()
135-
issuanceSession = null
135+
discardCurrentIssuance()
136136
_state.update {
137137
it.copy(
138138
selectedTab = WalletDemoTab.Receive,
@@ -156,6 +156,7 @@ class WalletDemoController(
156156
}
157157
WalletDeepLinkScheme.PresentationRequest -> {
158158
receiveJob?.cancel()
159+
discardCurrentIssuance()
159160
_state.update {
160161
it.copy(
161162
selectedTab = WalletDemoTab.Present,
@@ -176,7 +177,7 @@ class WalletDemoController(
176177

177178
fun startNewReceiveFlow() {
178179
receiveJob?.cancel()
179-
issuanceSession = null
180+
discardCurrentIssuance()
180181
_state.update {
181182
it.copy(
182183
requestDrafts = it.requestDrafts.copy(
@@ -196,6 +197,7 @@ class WalletDemoController(
196197

197198
fun startNewPresentationFlow() {
198199
receiveJob?.cancel()
200+
discardCurrentIssuance()
199201
_state.update {
200202
it.copy(
201203
requestDrafts = it.requestDrafts.copy(presentationRequestUrl = ""),
@@ -330,6 +332,8 @@ class WalletDemoController(
330332
it.copy(
331333
offerPreview = null,
332334
authorizationRequestUrl = null,
335+
deferredCredentials = (it.deferredCredentials + outcome.credentials)
336+
.distinctBy(WalletDemoDeferredCredential::id),
333337
operation = WalletOperationState.Succeeded(
334338
message = "Credential issuance deferred",
335339
tab = WalletDemoTab.Receive,
@@ -409,6 +413,85 @@ class WalletDemoController(
409413
_state.update { it.copy(authorizationRequestUrl = null) }
410414
}
411415

416+
fun resumeDeferredCredential(deferredCredentialId: String) {
417+
val current = _state.value
418+
val ready = current.session as? WalletSessionState.Ready ?: return
419+
if (current.deferredCredentials.none { it.id == deferredCredentialId }) return
420+
if (!_state.compareAndSet(current, current.copy(operation = WalletOperationState.Receiving))) return
421+
422+
receiveJob = scope.launch(dispatcher) {
423+
try {
424+
when (val outcome = wallet.resumeDeferredIssuance(deferredCredentialId)) {
425+
is WalletDemoIssuanceOutcome.Stored -> {
426+
val refreshed = wallet.listCredentials()
427+
_state.update {
428+
it.copy(
429+
session = ready.copy(credentials = refreshed),
430+
deferredCredentials = it.deferredCredentials.filterNot { pending ->
431+
pending.id == deferredCredentialId
432+
},
433+
lastReceivedCredentialIds = outcome.credentialIds,
434+
receiveCompleted = true,
435+
operation = WalletOperationState.Succeeded(
436+
WalletDisplayText.receivedCredentials(outcome.credentialIds.size),
437+
WalletDemoTab.Receive,
438+
),
439+
)
440+
}
441+
}
442+
is WalletDemoIssuanceOutcome.Deferred -> _state.update {
443+
it.copy(
444+
deferredCredentials = (
445+
it.deferredCredentials.filterNot { pending -> pending.id == deferredCredentialId } +
446+
outcome.credentials
447+
).distinctBy(WalletDemoDeferredCredential::id),
448+
operation = WalletOperationState.Succeeded(
449+
"Credential issuance still pending",
450+
WalletDemoTab.Receive,
451+
),
452+
)
453+
}
454+
WalletDemoIssuanceOutcome.Cancelled -> _state.update {
455+
it.copy(
456+
deferredCredentials = it.deferredCredentials.filterNot { pending ->
457+
pending.id == deferredCredentialId
458+
},
459+
operation = WalletOperationState.Succeeded(
460+
WalletDisplayText.CredentialOfferDeclined,
461+
WalletDemoTab.Receive,
462+
),
463+
)
464+
}
465+
is WalletDemoIssuanceOutcome.Failed -> _state.update {
466+
it.copy(
467+
operation = WalletOperationState.Failed(
468+
WalletDisplayText.failure(WalletDisplayText.ReceiveFailed, outcome.message),
469+
WalletDemoTab.Receive,
470+
)
471+
)
472+
}
473+
}
474+
} catch (cancellation: CancellationException) {
475+
throw cancellation
476+
} catch (error: Throwable) {
477+
_state.update {
478+
it.copy(
479+
operation = WalletOperationState.Failed(
480+
WalletDisplayText.failure(WalletDisplayText.ReceiveFailed, error),
481+
WalletDemoTab.Receive,
482+
)
483+
)
484+
}
485+
}
486+
}
487+
}
488+
489+
private fun discardCurrentIssuance() {
490+
val sessionId = issuanceSession?.id ?: return
491+
issuanceSession = null
492+
scope.launch(dispatcher) { wallet.cancelIssuance(sessionId) }
493+
}
494+
412495
private fun continueAuthorization(callbackUri: String) {
413496
val session = issuanceSession ?: return
414497
val current = _state.value

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/commonMain/kotlin/id/walt/walletdemo/compose/logic/WalletDemoIssuanceModels.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@ data class WalletDemoIssuanceSession(
99
val authorizationUrl: String? = null,
1010
)
1111

12+
data class WalletDemoDeferredCredential(
13+
val id: String,
14+
val credentialConfigurationId: String,
15+
val intervalSeconds: Long?,
16+
)
17+
1218
sealed interface WalletDemoIssuanceOutcome {
1319
data class Stored(val credentialIds: List<String>) : WalletDemoIssuanceOutcome
14-
data class Deferred(val storedCredentialIds: List<String>) : WalletDemoIssuanceOutcome
20+
data class Deferred(
21+
val storedCredentialIds: List<String>,
22+
val credentials: List<WalletDemoDeferredCredential> = emptyList(),
23+
) : WalletDemoIssuanceOutcome
1524
data object Cancelled : WalletDemoIssuanceOutcome
1625
data class Failed(val message: String) : WalletDemoIssuanceOutcome
1726
}

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/commonMain/kotlin/id/walt/walletdemo/compose/logic/WalletDemoUiState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ data class WalletDemoUiState(
99
val requestDrafts: WalletRequestDrafts = WalletRequestDrafts(),
1010
val offerPreview: WalletDemoOfferPreview? = null,
1111
val authorizationRequestUrl: String? = null,
12+
val deferredCredentials: List<WalletDemoDeferredCredential> = emptyList(),
1213
val lastReceivedCredentialIds: List<String> = emptyList(),
1314
val receiveCompleted: Boolean = false,
1415
val receiveNavigationResetKey: Int = 0,

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/commonTest/kotlin/id/walt/walletdemo/compose/logic/WalletDemoControllerTest.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,10 @@ class WalletDemoControllerTest {
266266

267267
@Test
268268
fun deferredIssuanceProducesTypedNonErrorResult() = runTest {
269+
val pending = WalletDemoDeferredCredential("deferred-1", "ExampleCredential", 1)
269270
val wallet = FakeDemoWallet(
270-
preAuthorizedOutcome = WalletDemoIssuanceOutcome.Deferred(emptyList()),
271+
preAuthorizedOutcome = WalletDemoIssuanceOutcome.Deferred(emptyList(), listOf(pending)),
272+
deferredOutcome = WalletDemoIssuanceOutcome.Stored(listOf("cred-1")),
271273
)
272274
val controller = unlockedControllerWith(wallet, this)
273275

@@ -282,7 +284,16 @@ class WalletDemoControllerTest {
282284
controller.state.value.operation,
283285
)
284286
assertFalse(controller.state.value.receiveCompleted)
287+
assertEquals(listOf(pending), controller.state.value.deferredCredentials)
285288
assertEquals(0, wallet.receiveCalls)
289+
290+
wallet.credentials = listOf(sampleCredential)
291+
controller.resumeDeferredCredential(pending.id)
292+
runCurrent()
293+
294+
assertTrue(controller.state.value.receiveCompleted)
295+
assertEquals(emptyList(), controller.state.value.deferredCredentials)
296+
assertEquals("deferred-1", wallet.resumedDeferredCredentialId)
286297
}
287298

288299
@Test
@@ -296,9 +307,11 @@ class WalletDemoControllerTest {
296307
controller.updateTxCode("1234")
297308

298309
controller.updateOfferUrl("openid-credential-offer://second")
310+
runCurrent()
299311

300312
assertEquals("", controller.state.value.requestDrafts.txCode)
301313
assertFalse(controller.state.value.requestDrafts.transactionCodeRequired)
314+
assertEquals(listOf("session-1"), wallet.cancelledIssuanceSessionIds)
302315
}
303316

304317
@Test
@@ -1195,6 +1208,7 @@ private class FakeDemoWallet(
11951208
private val authorizationUrl: String? = null,
11961209
private val preAuthorizedOutcome: WalletDemoIssuanceOutcome? = null,
11971210
private val authorizationOutcome: WalletDemoIssuanceOutcome = WalletDemoIssuanceOutcome.Stored(listOf("cred-1")),
1211+
private val deferredOutcome: WalletDemoIssuanceOutcome = WalletDemoIssuanceOutcome.Failed("No deferred credential"),
11981212
private val presentationResult: WalletDemoOperationResult = WalletDemoOperationResult.Success(WalletDisplayText.PresentationSent),
11991213
private val presentationPreview: WalletDemoPresentationPreview = WalletDemoPresentationPreview(
12001214
verifierName = null,
@@ -1210,6 +1224,8 @@ private class FakeDemoWallet(
12101224
var receiveCalls = 0
12111225
var continuedAuthorizationSessionId: String? = null
12121226
var authorizationCallbackUri: String? = null
1227+
var resumedDeferredCredentialId: String? = null
1228+
val cancelledIssuanceSessionIds = mutableListOf<String>()
12131229
var presentedRequestUrl: String? = null
12141230
var previewedRequestUrl: String? = null
12151231
var submittedRequestUrl: String? = null
@@ -1261,6 +1277,16 @@ private class FakeDemoWallet(
12611277
return authorizationOutcome
12621278
}
12631279

1280+
override suspend fun resumeDeferredIssuance(deferredCredentialId: String): WalletDemoIssuanceOutcome {
1281+
resumedDeferredCredentialId = deferredCredentialId
1282+
return deferredOutcome
1283+
}
1284+
1285+
override suspend fun cancelIssuance(sessionId: String): WalletDemoIssuanceOutcome {
1286+
cancelledIssuanceSessionIds += sessionId
1287+
return WalletDemoIssuanceOutcome.Cancelled
1288+
}
1289+
12641290
override suspend fun receive(offerUrl: String, txCode: String?): List<String> {
12651291
receiveCalls += 1
12661292
receivedOfferUrl = offerUrl

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/mobileMain/kotlin/id/walt/walletdemo/compose/logic/MobileDemoWallet.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ internal class MobileDemoWallet(
8888
override suspend fun cancelIssuance(sessionId: String): WalletDemoIssuanceOutcome =
8989
mobileWallet.cancelIssuance(sessionId).toDemoOutcome()
9090

91+
override suspend fun resumeDeferredIssuance(deferredCredentialId: String): WalletDemoIssuanceOutcome =
92+
mobileWallet.resumeDeferredIssuance(deferredCredentialId).toDemoOutcome()
93+
9194
override suspend fun present(requestUrl: String, did: String?): WalletDemoOperationResult =
9295
mobileWallet.present(requestUrl = requestUrl, did = did).let { result ->
9396
if (result.success) {
@@ -134,7 +137,16 @@ internal class MobileDemoWallet(
134137

135138
private fun WalletIssuanceOutcome.toDemoOutcome(): WalletDemoIssuanceOutcome = when (this) {
136139
is WalletIssuanceOutcome.Stored -> WalletDemoIssuanceOutcome.Stored(credentialIds)
137-
is WalletIssuanceOutcome.Deferred -> WalletDemoIssuanceOutcome.Deferred(storedCredentialIds)
140+
is WalletIssuanceOutcome.Deferred -> WalletDemoIssuanceOutcome.Deferred(
141+
storedCredentialIds = storedCredentialIds,
142+
credentials = credentials.map {
143+
WalletDemoDeferredCredential(
144+
id = it.id,
145+
credentialConfigurationId = it.credentialConfigurationId,
146+
intervalSeconds = it.intervalSeconds,
147+
)
148+
},
149+
)
138150
is WalletIssuanceOutcome.Cancelled -> WalletDemoIssuanceOutcome.Cancelled
139151
is WalletIssuanceOutcome.Failed -> WalletDemoIssuanceOutcome.Failed(error.message)
140152
}

waltid-applications/waltid-wallet-demo-compose/sharedLogic/src/wasmJsMain/kotlin/id/walt/walletdemo/compose/logic/MockDemoWallet.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ private class MockDemoWallet : DemoWallet {
5050

5151
override suspend fun cancelIssuance(sessionId: String) = WalletDemoIssuanceOutcome.Cancelled
5252

53+
override suspend fun resumeDeferredIssuance(deferredCredentialId: String) =
54+
WalletDemoIssuanceOutcome.Failed("No mock deferred credential")
55+
5356
override suspend fun present(requestUrl: String, did: String?): WalletDemoOperationResult =
5457
WalletDemoOperationResult.Success("Mock presentation sent")
5558

waltid-applications/waltid-wallet-demo-compose/sharedUI/src/commonMain/kotlin/id/walt/walletdemo/compose/ui/screens/ReceiveTab.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal fun ReceiveTab(
3838
onAcceptOffer: () -> Unit,
3939
onDeclineOffer: () -> Unit,
4040
onStartNew: () -> Unit,
41+
onResumeDeferred: (String) -> Unit,
4142
onCredentialClick: (String) -> Unit,
4243
) {
4344
val receivedCredentials = state.receivedCredentials()
@@ -76,6 +77,18 @@ internal fun ReceiveTab(
7677
)
7778
}
7879

80+
if (state.deferredCredentials.isNotEmpty()) {
81+
Text("Pending credentials", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
82+
state.deferredCredentials.forEach { pending ->
83+
OutlinedButton(
84+
onClick = { onResumeDeferred(pending.id) },
85+
enabled = !state.isAuthenticating,
86+
) {
87+
Text("Check ${pending.credentialConfigurationId}")
88+
}
89+
}
90+
}
91+
7992
if (state.receiveCompleted) {
8093
OutlinedButton(
8194
onClick = onStartNew,

waltid-applications/waltid-wallet-demo-compose/sharedUI/src/commonMain/kotlin/id/walt/walletdemo/compose/ui/screens/WalletScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ internal fun WalletScreen(controller: WalletDemoController, state: WalletDemoUiS
8989
onAcceptOffer = controller::acceptOffer,
9090
onDeclineOffer = controller::declineOffer,
9191
onStartNew = controller::startNewReceiveFlow,
92+
onResumeDeferred = controller::resumeDeferredCredential,
9293
onCredentialClick = { detailsId -> receiveBackStack.pushDetails(detailsId) },
9394
)
9495
},

0 commit comments

Comments
 (0)