Skip to content

Commit 21abd88

Browse files
authored
feat(decrypt): smooth transitions between Yivi, decrypt, and done states (#249)
Hold the Yivi success animation briefly before flipping to the progress banner, fade between states, and keep the success banner visible through Done with a distinct completion message.
1 parent a23d1cb commit 21abd88

3 files changed

Lines changed: 192 additions & 116 deletions

File tree

src/lib/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
"askDownload": "Download",
170170
"askDownloadText": "Press the button below to start decrypting and downloading your files",
171171
"doneMessage": "Your files are being downloaded and decrypted",
172+
"doneMessageComplete": "Your files have been downloaded and decrypted",
172173
"verifiedEmail": "The files are from",
173174
"verifiedExtra": "The files were signed using",
174175
"notFoundTitle": "These files no longer exist",

src/lib/locales/nl.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
"askDownload": "Download",
170170
"askDownloadText": "Druk op onderstaande knop om de bestanden te ontsleutelen en downloaden.",
171171
"doneMessage": "Je bestanden worden gedownload en ontsleuteld",
172+
"doneMessageComplete": "Je bestanden zijn gedownload en ontsleuteld",
172173
"verifiedEmail": "De bestanden komen van",
173174
"verifiedExtra": "De bestanden zijn ondertekend met",
174175
"notFoundTitle": "Deze bestanden bestaan niet (meer)",

src/routes/(app)/download/+page.svelte

Lines changed: 190 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { onMount, tick } from 'svelte'
3+
import { fade, slide } from 'svelte/transition'
34
import { browser, dev } from '$app/environment'
45
import { _ } from 'svelte-i18n'
56
import { pg, retryStatus } from '$lib/postguard'
@@ -44,6 +45,11 @@
4445
4546
let opened: Awaited<ReturnType<typeof pg.open>> | null = null
4647
48+
// Hold on the QR card briefly after Yivi succeeds so its success
49+
// animation can finish before we swap in the progress banner.
50+
const YIVI_SUCCESS_HOLD_MS = 1400
51+
let pendingDecryptFlip: ReturnType<typeof setTimeout> | null = null
52+
4753
let isMobileDevice = isMobile()
4854
4955
onMount(() => {
@@ -121,12 +127,18 @@
121127
recipient: key,
122128
enableCache: true,
123129
onDownloadProgress: (pct) => {
124-
// Yivi scan happens before any bytes flow — the first
125-
// progress tick is our cue to flip into Decrypting.
126-
if (downloadState !== 'Decrypting') {
127-
downloadState = 'Decrypting'
128-
}
129130
decryptPct = pct
131+
if (
132+
downloadState !== 'Decrypting' &&
133+
pendingDecryptFlip === null
134+
) {
135+
pendingDecryptFlip = setTimeout(() => {
136+
pendingDecryptFlip = null
137+
if (downloadState === 'Ready') {
138+
downloadState = 'Decrypting'
139+
}
140+
}, YIVI_SUCCESS_HOLD_MS)
141+
}
130142
},
131143
})) as DecryptFileResult
132144
@@ -226,123 +238,160 @@
226238
/>
227239
</div>
228240
{:else if downloadState === 'Ready'}
229-
<p class="description">
230-
{$_('filesharing.decryptpanel.pageDescription')}
231-
</p>
232-
<div class="decrypt-card">
233-
<h3>
234-
{isMobileDevice
235-
? $_(
236-
'filesharing.decryptpanel.irmaInstructionHeaderMobile'
237-
)
238-
: $_(
239-
'filesharing.decryptpanel.irmaInstructionHeaderQr'
240-
)}
241-
</h3>
242-
<p class="card-subtitle">
243-
{isMobileDevice
244-
? $_('filesharing.decryptpanel.irmaInstructionMobile')
245-
: $_('filesharing.decryptpanel.irmaInstructionQr')}
241+
<div
242+
class="state-wrap"
243+
in:fade={{ duration: 250, delay: 200 }}
244+
out:fade={{ duration: 200 }}
245+
>
246+
<p class="description">
247+
{$_('filesharing.decryptpanel.pageDescription')}
246248
</p>
247-
<YiviQRCode
248-
id="yivi-download"
249-
responsive
250-
mode={isMobileDevice ? 'deeplink' : 'qr'}
251-
/>
252-
</div>
253-
<HelpToggle
254-
title={$_('filesharing.encryptPanel.yiviInfo')}
255-
content={$_('filesharing.encryptPanel.yiviInfoText')}
256-
linkText={$_('filesharing.encryptPanel.yiviInfoLink')}
257-
linkUrl="https://yivi.app"
258-
bordered
259-
/>
260-
{#if senderIdentity?.email}
261-
<div class="sender-section">
262-
<svg
263-
class="checkmark"
264-
viewBox="0 0 12 10"
265-
fill="none"
266-
xmlns="http://www.w3.org/2000/svg"
267-
>
268-
<path
269-
d="M1 5L4.5 8.5L11 1"
270-
stroke="currentColor"
271-
stroke-width="1.75"
272-
stroke-linecap="round"
273-
stroke-linejoin="round"
274-
/>
275-
</svg>
276-
<p class="sender-label">
277-
{$_('filesharing.decryptpanel.verifiedEmail')}
249+
<div class="decrypt-card">
250+
<h3>
251+
{isMobileDevice
252+
? $_(
253+
'filesharing.decryptpanel.irmaInstructionHeaderMobile'
254+
)
255+
: $_(
256+
'filesharing.decryptpanel.irmaInstructionHeaderQr'
257+
)}
258+
</h3>
259+
<p class="card-subtitle">
260+
{isMobileDevice
261+
? $_(
262+
'filesharing.decryptpanel.irmaInstructionMobile'
263+
)
264+
: $_('filesharing.decryptpanel.irmaInstructionQr')}
278265
</p>
279-
<strong class="sender-email">{senderIdentity.email}</strong>
280-
</div>
281-
{/if}
282-
{:else if downloadState === 'Decrypting'}
283-
<div class="decrypt-card">
284-
<DecryptionProgress percentage={decryptPct} />
285-
</div>
286-
{#if $retryStatus}
287-
<p class="retry-status" role="status">
288-
{$_('filesharing.encryptPanel.retrying', {
289-
values: {
290-
attempt: $retryStatus.attempt + 1,
291-
max: $retryStatus.maxAttempts,
292-
},
293-
})}
294-
</p>
295-
{/if}
296-
{:else if downloadState === 'Done'}
297-
<div class="success-banner">
298-
<svg
299-
class="banner-check"
300-
viewBox="0 0 12 10"
301-
fill="none"
302-
xmlns="http://www.w3.org/2000/svg"
303-
>
304-
<path
305-
d="M1 5L4.5 8.5L11 1"
306-
stroke="currentColor"
307-
stroke-width="1.75"
308-
stroke-linecap="round"
309-
stroke-linejoin="round"
266+
<YiviQRCode
267+
id="yivi-download"
268+
responsive
269+
mode={isMobileDevice ? 'deeplink' : 'qr'}
310270
/>
311-
</svg>
312-
<p>{$_('filesharing.decryptpanel.doneMessage')}</p>
271+
</div>
272+
<HelpToggle
273+
title={$_('filesharing.encryptPanel.yiviInfo')}
274+
content={$_('filesharing.encryptPanel.yiviInfoText')}
275+
linkText={$_('filesharing.encryptPanel.yiviInfoLink')}
276+
linkUrl="https://yivi.app"
277+
bordered
278+
/>
279+
{#if senderIdentity?.email}
280+
<div class="sender-section">
281+
<svg
282+
class="checkmark"
283+
viewBox="0 0 12 10"
284+
fill="none"
285+
xmlns="http://www.w3.org/2000/svg"
286+
>
287+
<path
288+
d="M1 5L4.5 8.5L11 1"
289+
stroke="currentColor"
290+
stroke-width="1.75"
291+
stroke-linecap="round"
292+
stroke-linejoin="round"
293+
/>
294+
</svg>
295+
<p class="sender-label">
296+
{$_('filesharing.decryptpanel.verifiedEmail')}
297+
</p>
298+
<strong class="sender-email"
299+
>{senderIdentity.email}</strong
300+
>
301+
</div>
302+
{/if}
313303
</div>
314-
315-
<FileList files={fileList} />
316-
317-
{#if senderIdentity?.email}
318-
<div class="sender-section">
319-
<svg
320-
class="checkmark"
321-
viewBox="0 0 12 10"
322-
fill="none"
323-
xmlns="http://www.w3.org/2000/svg"
324-
>
325-
<path
326-
d="M1 5L4.5 8.5L11 1"
327-
stroke="currentColor"
328-
stroke-width="1.75"
329-
stroke-linecap="round"
330-
stroke-linejoin="round"
331-
/>
332-
</svg>
333-
<p class="sender-label">
334-
{$_('filesharing.decryptpanel.verifiedEmail')}
335-
</p>
336-
<strong class="sender-email">{senderIdentity.email}</strong>
337-
{#if senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value).length > 0}
338-
<div class="attr-chips">
339-
{#each senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value) as attr (attr.type)}
340-
<span class="attr-chip">{attr.value}</span>
341-
{/each}
304+
{:else if downloadState === 'Decrypting' || downloadState === 'Done'}
305+
<div
306+
class="state-wrap"
307+
in:fade={{ duration: 300, delay: 200 }}
308+
out:fade={{ duration: 200 }}
309+
>
310+
<div class="success-banner">
311+
<div class="banner-row">
312+
{#if downloadState === 'Done'}
313+
<svg
314+
class="banner-check"
315+
viewBox="0 0 12 10"
316+
fill="none"
317+
xmlns="http://www.w3.org/2000/svg"
318+
in:fade={{ duration: 250, delay: 100 }}
319+
>
320+
<path
321+
d="M1 5L4.5 8.5L11 1"
322+
stroke="currentColor"
323+
stroke-width="1.75"
324+
stroke-linecap="round"
325+
stroke-linejoin="round"
326+
/>
327+
</svg>
328+
{/if}
329+
<p>
330+
{downloadState === 'Done'
331+
? $_(
332+
'filesharing.decryptpanel.doneMessageComplete'
333+
)
334+
: $_('filesharing.decryptpanel.doneMessage')}
335+
</p>
336+
</div>
337+
{#if downloadState === 'Decrypting'}
338+
<div transition:slide={{ duration: 280 }}>
339+
<DecryptionProgress percentage={decryptPct} />
342340
</div>
343341
{/if}
344342
</div>
345-
{/if}
343+
{#if downloadState === 'Decrypting' && $retryStatus}
344+
<p class="retry-status" role="status">
345+
{$_('filesharing.encryptPanel.retrying', {
346+
values: {
347+
attempt: $retryStatus.attempt + 1,
348+
max: $retryStatus.maxAttempts,
349+
},
350+
})}
351+
</p>
352+
{/if}
353+
354+
{#if downloadState === 'Done'}
355+
<div in:fade={{ duration: 300, delay: 200 }}>
356+
<FileList files={fileList} />
357+
</div>
358+
{/if}
359+
360+
{#if downloadState === 'Done' && senderIdentity?.email}
361+
<div
362+
class="sender-section"
363+
in:fade={{ duration: 300, delay: 280 }}
364+
>
365+
<svg
366+
class="checkmark"
367+
viewBox="0 0 12 10"
368+
fill="none"
369+
xmlns="http://www.w3.org/2000/svg"
370+
>
371+
<path
372+
d="M1 5L4.5 8.5L11 1"
373+
stroke="currentColor"
374+
stroke-width="1.75"
375+
stroke-linecap="round"
376+
stroke-linejoin="round"
377+
/>
378+
</svg>
379+
<p class="sender-label">
380+
{$_('filesharing.decryptpanel.verifiedEmail')}
381+
</p>
382+
<strong class="sender-email"
383+
>{senderIdentity.email}</strong
384+
>
385+
{#if senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value).length > 0}
386+
<div class="attr-chips">
387+
{#each senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value) as attr (attr.type)}
388+
<span class="attr-chip">{attr.value}</span>
389+
{/each}
390+
</div>
391+
{/if}
392+
</div>
393+
{/if}
394+
</div>
346395
{:else if downloadState === 'SessionExpired'}
347396
<p class="error-description">
348397
{$_('filesharing.decryptpanel.sessionExpiredSubtitle')}
@@ -531,7 +580,7 @@
531580
532581
.success-banner {
533582
display: flex;
534-
align-items: center;
583+
flex-direction: column;
535584
gap: 0.75rem;
536585
background: var(--pg-strong-background);
537586
border-radius: var(--pg-border-radius-lg);
@@ -546,6 +595,31 @@
546595
}
547596
}
548597
598+
.banner-row {
599+
display: flex;
600+
align-items: center;
601+
gap: 0.75rem;
602+
position: relative;
603+
604+
p {
605+
margin: 0;
606+
}
607+
}
608+
609+
.state-wrap {
610+
display: flex;
611+
flex-direction: column;
612+
gap: 1.25rem;
613+
}
614+
615+
.success-banner :global(.container) {
616+
padding: 0;
617+
}
618+
619+
.success-banner :global(.container .label) {
620+
display: none;
621+
}
622+
549623
.banner-check {
550624
width: 14px;
551625
height: 14px;

0 commit comments

Comments
 (0)