Cleanup and refactoring (#60)

* Use production infra

* Move theme out of subdirectory

* Remove unused example test

* Separate webnative module into init and account

* Move state off of module global state

* Move utils out of common directory

* Organize imports consistently

* Replace last remaining Toast with a Notification

* Rename theme to themeStore

Renaming keeps the theme store naming consistent with the other stores.

* Replace uuid helper with WebCrypto randomUUID

* Enfore minimum node version at >=16.9

* Refactor delegate-account and link-device routes

Use component views for consistency with the other routes

* Use FilesystemActivity component in Register

* Remove console logs

* Add extractSearchParam

Generalizes extracting a search param from a URL.

* Hide app name in header on mobile

* Remove deviceStore

We weren't using it in the app. Mobile styles are handled with tailwind
utility classes.

* Add social preview image

* Remove navigate event from AreYouSure component

The event was unused.

* Replace convertUint8ToString with uint8arrays

* Add initializeFilesystem

Separates out app-specific resource creation from the required
registration code

* Allow minor webnative version upgrades

* Update package-lock.json
This commit is contained in:
Brian Ginsburg 2022-09-15 12:52:24 -07:00 committed by GitHub
parent 05b70fd159
commit 102ecd9b3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 720 additions and 597 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

187
package-lock.json generated
View File

@ -10,7 +10,8 @@
"dependencies": {
"clipboard-copy": "^4.0.1",
"qrcode-svg": "^1.1.0",
"webnative": "0.34.1"
"uint8arrays": "^3.1.0",
"webnative": "^0.34.1"
},
"devDependencies": {
"@sveltejs/adapter-static": "1.0.0-next.36",
@ -37,6 +38,9 @@
"tslib": "^2.0.0",
"typescript": "^4.4.4",
"vite": "^3.0.0"
},
"engines": {
"node": ">=16.9"
}
},
"node_modules/@babel/code-frame": {
@ -47,6 +51,100 @@
"@babel/highlight": "^7.10.4"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
@ -3235,6 +3333,12 @@
"node": ">= 0.8"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"node_modules/js-yaml": {
"version": "3.14.1",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
@ -5712,6 +5816,81 @@
"@babel/highlight": "^7.10.4"
}
},
"@babel/helper-validator-identifier": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
"dev": true
},
"@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"@cspotcode/source-map-support": {
"version": "0.8.1",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
@ -7996,6 +8175,12 @@
"integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"js-yaml": {
"version": "3.14.1",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",

View File

@ -53,6 +53,10 @@
"dependencies": {
"clipboard-copy": "^4.0.1",
"qrcode-svg": "^1.1.0",
"webnative": "0.34.1"
"uint8arrays": "^3.1.0",
"webnative": "^0.34.1"
},
"engines": {
"node": ">=16.9"
}
}

View File

@ -1,16 +1,16 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { sessionStore, theme } from '../stores'
import { storeTheme, type Theme } from '$lib/theme/index'
import { appName } from '$lib/app-info'
import { sessionStore, themeStore } from '../stores'
import { storeTheme, type Theme } from '$lib/theme'
import Brand from '$components/icons/Brand.svelte'
import Shield from '$components/icons/Shield.svelte'
import LightMode from '$components/icons/LightMode.svelte'
import DarkMode from '$components/icons/DarkMode.svelte'
import LightMode from '$components/icons/LightMode.svelte'
import Shield from '$components/icons/Shield.svelte'
const setTheme = (newTheme: Theme) => {
theme.set(newTheme)
themeStore.set(newTheme)
storeTheme(newTheme)
}
</script>
@ -18,7 +18,7 @@
<header class="navbar bg-base-100 pt-0">
<div class="flex-1 cursor-pointer hover:underline" on:click={() => goto('/')}>
<Brand />
<span class="text-xl ml-2">{appName}</span>
<span class="text-xl ml-2 hidden md:inline">{appName}</span>
</div>
{#if !$sessionStore.loading && !$sessionStore.authed}
@ -40,7 +40,7 @@
{/if}
<span class="ml-2">
{#if $theme === 'light'}
{#if $themeStore === 'light'}
<span on:click={() => setTheme('dark')}>
<LightMode />
</span>

View File

@ -1,16 +1,8 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { goto } from '$app/navigation'
import { filesystemStore, sessionStore } from '../../../stores'
import { setBackupStatus } from '$lib/auth/backup'
import type { BackupView } from '$lib/views'
import { goto } from '$app/navigation'
const dispatch = createEventDispatcher()
const navigate = (view: BackupView) => {
dispatch('navigate', { view })
}
const skipBackup = () => {
setBackupStatus($filesystemStore, { created: false })

View File

@ -0,0 +1,41 @@
<script lang="ts">
import { goto } from '$app/navigation'
import clipboardCopy from 'clipboard-copy'
import ClipboardIcon from '$components/icons/ClipboardIcon.svelte'
export let qrcode
export let connectionLink: string
export let backupCreated: boolean
const copyLink = async () => {
await clipboardCopy(connectionLink)
}
</script>
<input type="checkbox" id="backup-device-modal" checked class="modal-toggle" />
<div class="modal">
<div
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
>
<div>
<h3 class="pb-1 text-xl font-serif">Connect a backup device</h3>
{@html qrcode}
<p class="pt-1 mb-8">
Scan this code on the new device, or share the connection link.
</p>
<button class="btn btn-primary btn-outline" on:click={copyLink}>
<ClipboardIcon />
<span class="ml-2">Copy connection link</span>
</button>
{#if !backupCreated}
<button
class="btn btn-xs btn-link text-base text-error font-normal underline mt-4"
on:click={() => goto('/backup?view=are-you-sure')}
>
Skip for now
</button>
{/if}
</div>
</div>
</div>

View File

@ -0,0 +1,64 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let pinInput: string
export let pinError: boolean
const dispatch = createEventDispatcher()
const cancelConnection = () => {
dispatch('cancel')
}
const checkPin = () => {
dispatch('checkpin')
}
</script>
<input
type="checkbox"
id="delegate-account-modal"
checked
class="modal-toggle"
/>
<div class="modal">
<div
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
>
<div>
<h3 class="mb-7 text-xl font-serif">
A new device would like to connect to your account
</h3>
<div class="mb-5">
<input
id="pin"
type="text"
class="input input-bordered w-full max-w-xs mb-2 rounded-full h-16 font-mono text-3xl text-center tracking-[0.18em] font-light dark:border-slate-300"
bind:value={pinInput}
/>
<label for="pin" class="label">
{#if !pinError}
<span class="label-text-alt text-slate-500">
Enter the connection code to approve the connection.
</span>
{:else}
<span class="label-text-alt text-error">
Entered pin does not match a pin from a known device.
</span>
{/if}
</label>
</div>
<div>
<button class="btn btn-primary mb-5 w-full" on:click={checkPin}>
Approve the connection
</button>
<button
class="btn btn-primary btn-outline w-full"
on:click={cancelConnection}
>
Cancel Request
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,48 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let pin: string
const dispatch = createEventDispatcher()
const cancelConnection = () => {
dispatch('cancel')
}
</script>
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
<div class="modal">
<div
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
>
<div class="grid grid-flow-row auto-rows-max gap-7">
<h3 class="text-xl font-serif">Connection Requested</h3>
<div class="grid grid-flow-row auto-rows-max gap-4 justify-items-center">
{#if pin}
<span
class="btn bg-blue-100 dark:bg-blue-900 hover:bg-blue-100 dark:hover:bg-blue-900 border-0 btn-lg rounded-full text-3xl tracking-[.18em] w-3/4 cursor-default font-mono font-light"
>
{pin}
</span>
{/if}
<span class="text-md">Enter this code on your connected device.</span>
<div
class="grid grid-flow-col auto-cols-max gap-4 justify-center items-center text-slate-500"
>
<span
class="rounded-lg border-t-2 border-l-2 border-slate-600 dark:border-slate-50 w-4 h-4 block animate-spin"
/>
Waiting for a response...
</div>
</div>
<div>
<button
class="btn btn-primary btn-outline text-base font-normal mt-4"
on:click={cancelConnection}
>
Cancel Request
</button>
</div>
</div>
</div>
</div>

View File

@ -1,196 +0,0 @@
<script lang="ts">
import clipboardCopy from 'clipboard-copy'
import QRCode from 'qrcode-svg'
import { goto } from '$app/navigation'
import { onDestroy, onMount } from 'svelte'
import { addNotification } from '$lib/notifications'
import { createAccountLinkingProducer } from '$lib/auth/linking'
import { filesystemStore, sessionStore, theme } from '../../../stores'
import { getBackupStatus, setBackupStatus } from '$lib/auth/backup'
import ClipboardIcon from '$components/icons/ClipboardIcon.svelte'
let view: 'backup-device' | 'delegate-account' = 'backup-device'
let connectionLink = null
let qrcode = null
let fs = null
let backupCreated = true
let pin: number[]
let pinInput = ''
let pinError = false
let confirmPin = () => {}
let rejectPin = () => {}
let unsubscribeFilesystemStore = () => {}
let unsubscribeSessionStore = () => {}
onMount(() => {
unsubscribeFilesystemStore = filesystemStore.subscribe(async val => {
fs = val
if (fs) {
const backupStatus = await getBackupStatus(fs)
backupCreated = backupStatus.created
}
})
unsubscribeSessionStore = sessionStore.subscribe(async val => {
const username = val.username
if (username) {
const origin = window.location.origin
connectionLink = `${origin}/link-device?username=${username}`
qrcode = new QRCode({
content: connectionLink,
color: $theme === 'light' ? '#334155' : '#E2E8F0',
background: '#ffffff00'
}).svg()
initAccountLinkingProducer(username)
}
})
})
const initAccountLinkingProducer = async (username: string) => {
const accountLinkingProducer = await createAccountLinkingProducer(username)
accountLinkingProducer.on('challenge', detail => {
pin = detail.pin
confirmPin = detail.confirmPin
rejectPin = detail.rejectPin
view = 'delegate-account'
})
accountLinkingProducer.on('link', async ({ approved }) => {
if (approved) {
sessionStore.update(session => ({
...session,
backupCreated: true
}))
if (fs) {
await setBackupStatus(fs, { created: true })
addNotification("You've connected a backup device!", 'success')
goto('/')
} else {
addNotification(
'Missing filesystem. Unable to create a backup device.',
'error'
)
}
}
})
}
const cancelConnection = () => {
rejectPin()
addNotification('The connection attempt was cancelled', 'info')
goto('/')
}
const copyLink = async () => {
await clipboardCopy(connectionLink)
}
const checkPin = () => {
if (pin.join('') === pinInput) {
confirmPin()
} else {
pinError = true
}
}
onDestroy(() => {
unsubscribeFilesystemStore()
unsubscribeSessionStore()
})
</script>
{#if view === 'backup-device'}
<input
type="checkbox"
id="backup-device-modal"
checked
class="modal-toggle"
/>
<div class="modal">
<div
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
>
<div>
<h3 class="pb-1 text-xl font-serif">Connect a backup device</h3>
{@html qrcode}
<p class="pt-1 mb-8">
Scan this code on the new device, or share the connection link.
</p>
<button class="btn btn-primary btn-outline" on:click={copyLink}>
<ClipboardIcon />
<span class="ml-2">Copy connection link</span>
</button>
{#if !backupCreated}
<button
class="btn btn-xs btn-link text-base text-error font-normal underline mt-4"
on:click={() => goto('/backup?view=are-you-sure')}
>
Skip for now
</button>
{/if}
</div>
</div>
</div>
{:else if view === 'delegate-account'}
<input
type="checkbox"
id="delegate-account-modal"
checked
class="modal-toggle"
/>
<div class="modal">
<div
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
>
<div>
<h3 class="mb-7 text-xl font-serif">
A new device would like to connect to your account
</h3>
<div class="mb-5">
<input
id="pin"
type="text"
class="input input-bordered w-full max-w-xs mb-2 rounded-full h-16 font-mono text-3xl text-center tracking-[0.18em] font-light dark:border-slate-300"
bind:value={pinInput}
/>
<label for="pin" class="label">
{#if !pinError}
<span class="label-text-alt text-slate-500">
Enter the connection code to approve the connection.
</span>
{:else}
<span class="label-text-alt text-error">
Entered pin does not match a pin from a known device.
</span>
{/if}
</label>
</div>
<div>
<button class="btn btn-primary mb-5 w-full" on:click={checkPin}>
Approve the connection
</button>
<button
class="btn btn-primary btn-outline w-full"
on:click={cancelConnection}
>
Cancel Request
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,106 +0,0 @@
<script lang="ts">
import type { account } from 'webnative'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { addNotification } from '$lib/notifications'
import { createAccountLinkingConsumer } from '$lib/auth/linking'
import { loadAccount } from '$lib/common/webnative'
let accountLinkingConsumer: account.AccountLinkingConsumer
let loadingFilesystem = false
let displayPin: string = ''
let url = $page.url
const username = url.searchParams.get('username')
// clear the params
url.searchParams.delete('username')
history.replaceState(null, document.title, url.toString())
const initAccountLinkingConsumer = async () => {
accountLinkingConsumer = await createAccountLinkingConsumer(username)
accountLinkingConsumer.on('challenge', ({ pin }) => {
displayPin = pin.join('')
})
accountLinkingConsumer.on('link', async ({ approved, username }) => {
if (approved) {
loadingFilesystem = true
await loadAccount(username)
addNotification("You're now connected!", 'success')
goto('/')
} else {
addNotification('The connection attempt was cancelled', 'info')
goto('/')
}
})
}
const cancelConnection = async () => {
addNotification('The connection attempt was cancelled', 'info')
await accountLinkingConsumer?.cancel()
goto('/')
}
initAccountLinkingConsumer()
</script>
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
{#if loadingFilesystem}
<div class="modal">
<div
class="modal-box rounded-lg shadow-sm bg-slate-100 w-80 relative text-center dark:bg-slate-900 dark:border-slate-600 dark:border "
>
<p class="text-slate-500 dark:text-slate-50">
<span
class="rounded-lg border-t-2 border-l-2 border-slate-500 dark:border-slate-50 w-4 h-4 inline-block animate-spin mr-1"
/>
Loading file system...
</p>
</div>
</div>
{:else}
<div class="modal">
<div
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
>
<div class="grid grid-flow-row auto-rows-max gap-7">
<h3 class="text-xl font-serif">Connection Requested</h3>
<div
class="grid grid-flow-row auto-rows-max gap-4 justify-items-center"
>
{#if displayPin}
<span
class="btn bg-blue-100 dark:bg-blue-900 hover:bg-blue-100 dark:hover:bg-blue-900 border-0 btn-lg rounded-full text-3xl tracking-[.18em] w-3/4 cursor-default font-mono font-light"
>
{displayPin}
</span>
{/if}
<span class="text-md">Enter this code on your connected device.</span>
<div
class="grid grid-flow-col auto-cols-max gap-4 justify-center items-center text-slate-500"
>
<span
class="rounded-lg border-t-2 border-l-2 border-slate-600 dark:border-slate-50 w-4 h-4 block animate-spin"
/>
Waiting for a response...
</div>
</div>
<div>
<button
class="btn btn-primary btn-outline text-base font-normal mt-4"
on:click={cancelConnection}
>
Cancel Request
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -4,9 +4,10 @@
isUsernameValid,
isUsernameAvailable,
register
} from '$lib/common/webnative'
} from '$lib/auth/account'
import CheckIcon from '$components/icons/CheckIcon.svelte'
import XIcon from '$components/icons/XIcon.svelte'
import FilesystemActivity from '$components/common/FilesystemActivity.svelte'
let username: string = ''
let usernameValid = true
@ -14,6 +15,8 @@
let registrationSuccess = true
let checkingUsername = false
let initializingFilesystem = false
const checkUsername = async (event: Event) => {
const { value } = event.target as HTMLInputElement
@ -28,30 +31,17 @@
checkingUsername = false
}
let authInProcess = false
const registerUser = async () => {
authInProcess = true
initializingFilesystem = true
registrationSuccess = await register(username)
if (!registrationSuccess) authInProcess = false
if (!registrationSuccess) initializingFilesystem = false
}
</script>
{#if authInProcess}
<input type="checkbox" id="initializing" checked class="modal-toggle" />
<div class="modal">
<div
class="modal-box rounded-lg shadow-sm bg-slate-100 w-80 relative text-center dark:bg-slate-900 dark:border-slate-600 dark:border "
>
<p class="text-slate-500 dark:text-slate-50">
<span
class="rounded-lg border-t-2 border-l-2 border-slate-500 dark:border-slate-50 w-4 h-4 inline-block animate-spin mr-1"
/>
Initializing file system...
</p>
</div>
</div>
{#if initializingFilesystem}
<FilesystemActivity activity="Initializing" />
{:else}
<input type="checkbox" id="register-modal" checked class="modal-toggle" />
<div class="modal">

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { appName } from '$lib/app-info'
import { sessionStore } from '../../../stores'
import WelcomeCheckIcon from '$components/icons/WelcomeCheckIcon.svelte'
import { appName } from '$lib/app-info'
</script>
<input type="checkbox" id="link-device-modal" checked class="modal-toggle" />

View File

@ -0,0 +1,17 @@
<script lang="ts">
export let activity: 'Initializing' | 'Loading'
</script>
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
<div class="modal">
<div
class="modal-box rounded-lg shadow-sm bg-slate-100 w-80 relative text-center dark:bg-slate-900 dark:border-slate-600 dark:border "
>
<p class="text-slate-500 dark:text-slate-50">
<span
class="rounded-lg border-t-2 border-l-2 border-slate-500 dark:border-slate-50 w-4 h-4 inline-block animate-spin mr-1"
/>
{activity} file system...
</p>
</div>
</div>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { onDestroy } from 'svelte'
import { galleryStore } from '../../../stores'
import { AREAS, getImagesFromWNFS } from '$lib/gallery'
import type { Image } from '$lib/gallery'

View File

@ -1,8 +1,9 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { deleteImageFromWNFS } from '$lib/gallery'
import { galleryStore } from '../../../stores'
import type { Gallery, Image } from '$lib/gallery'
import { deleteImageFromWNFS } from '$lib/gallery'
export let image: Image
export let isModalOpen: boolean = false

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { getImagesFromWNFS, uploadImageToWNFS } from '$lib/gallery'
import { addNotification } from '$lib/notifications'
import { getImagesFromWNFS, uploadImageToWNFS } from '$lib/gallery'
/**
* Detect when a user drags a file in or out of the dropzone to change the styles
@ -19,8 +19,6 @@
const files = Array.from(event.dataTransfer.items)
console.log(`${files.length} file${files.length > 1 ? 's' : ''} dropped`)
// Iterate over the dropped files and upload them to WNFS
await Promise.all(
files.map(async (item, index) => {
@ -30,9 +28,7 @@
// If the dropped files aren't images, we don't want them!
if (!file.type.match('image/*')) {
addNotification('Please upload images only', 'error')
console.error('Please upload images only')
} else {
console.log(`file[${index + 1}].name = ${file.name}`)
await uploadImageToWNFS(file)
}
}

View File

@ -1,10 +1,10 @@
<script lang="ts">
import { fade, fly } from 'svelte/transition'
import { themeStore } from '../../stores'
import type { Notification } from '$lib/notifications'
import CheckThinIcon from '$components/icons/CheckThinIcon.svelte'
import XThinIcon from '$components/icons/XThinIcon.svelte'
import { theme as themeStore } from '../../stores'
import type { Notification } from '$lib/notifications'
export let notification: Notification
</script>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { flip } from 'svelte/animate'
import Notification from '$components/notifications/Notification.svelte'
import { notificationStore } from '../../stores'
import Notification from '$components/notifications/Notification.svelte'
</script>
{#if $notificationStore.length}

View File

@ -1,37 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { quintOut } from 'svelte/easing'
import { fade } from 'svelte/transition'
import { theme } from '../../stores'
import CheckThinIcon from '$components/icons/CheckThinIcon.svelte'
import XThinIcon from '$components/icons/XThinIcon.svelte'
export let kind: 'success' | 'error'
export let message: string
const dispatch = createEventDispatcher()
const clearNotification = () => {
dispatch('clear')
}
</script>
<div
class="toast"
on:click={clearNotification}
out:fade={{ duration: 800, easing: quintOut }}
>
<div
class="alert {kind === 'success' ? 'alert-success' : 'alert-error'} text-sm"
>
<div>
{#if kind === 'success'}
<CheckThinIcon color={$theme === 'light' ? '#b8ffd3' : '#002e12'} />
{:else if kind === 'error'}
<XThinIcon color={$theme === 'light' ? '#ffd6d7' : '#fec3c3'} />
{/if}
<span class="pl-1">{message}</span>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
export const appName = 'Awesome Webnative App'
export const appDescription = 'This is another awesome Webnative app.'
export const appURL = 'https://webnative.netlify.app'
export const appImageURL = `${appURL}/preview.png`

View File

@ -1,75 +1,10 @@
import * as webnative from 'webnative'
import { setup } from 'webnative'
import type FileSystem from 'webnative/fs/index'
import { asyncDebounce } from '$lib/common/utils'
import { asyncDebounce } from '$lib/utils'
import { filesystemStore, sessionStore } from '../../stores'
import { getBackupStatus } from '$lib/auth/backup'
import { AREAS, GALLERY_DIRS } from '$lib/gallery'
import { getBackupStatus, type BackupStatus } from '$lib/auth/backup'
// runfission.net = staging
setup.endpoints({
api: 'https://runfission.net',
lobby: 'https://auth.runfission.net',
user: 'fissionuser.net'
})
let state: webnative.AppState
// TODO: Add a flag or script to turn debugging on/off
setup.debug({ enabled: false })
export const initialize = async (): Promise<void> => {
try {
let backupStatus: BackupStatus = null
state = await webnative.app({ useWnfs: true })
switch (state.scenario) {
case webnative.AppScenario.NotAuthed:
sessionStore.set({
username: '',
authed: false,
loading: false,
backupCreated: null
})
break
case webnative.AppScenario.Authed:
backupStatus = await getBackupStatus(state.fs)
sessionStore.set({
username: state.username,
authed: state.authenticated,
loading: false,
backupCreated: backupStatus.created
})
filesystemStore.set(state.fs)
break
default:
break
}
} catch (error) {
switch (error) {
case webnative.InitialisationError.InsecureContext:
sessionStore.update(session => ({
...session,
loading: false,
error: 'Insecure Context'
}))
break
case webnative.InitialisationError.UnsupportedBrowser:
sessionStore.update(session => ({
...session,
loading: false,
error: 'Unsupported Browser'
}))
break
}
}
}
export const isUsernameValid = async (username: string): Promise<boolean> => {
return webnative.account.isUsernameValid(username)
@ -94,9 +29,8 @@ export const register = async (username: string): Promise<boolean> => {
const fs = await webnative.bootstrapRootFileSystem()
filesystemStore.set(fs)
// Create public and private directories for the gallery
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PUBLIC]))
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PRIVATE]))
// TODO Remove if only public and private directories are needed
await initializeFilesystem(fs)
sessionStore.update(session => ({
...session,
@ -107,6 +41,16 @@ export const register = async (username: string): Promise<boolean> => {
return success
}
/**
* Create additional directories and files needed by the app
*
* @param fs FileSystem
*/
const initializeFilesystem = async (fs: FileSystem): Promise<void> => {
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PUBLIC]))
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PRIVATE]))
}
export const loadAccount = async (username: string): Promise<void> => {
await checkDataRoot(username)

View File

@ -1,5 +0,0 @@
import test from 'ava'
test('my passing test', t => {
t.pass()
})

View File

@ -1,52 +0,0 @@
export function asyncDebounce<A extends unknown[], R>(
fn: (...args: A) => Promise<R>,
wait: number
): (...args: A) => Promise<R> {
let lastTimeoutId: ReturnType<typeof setTimeout> | undefined = undefined
return (...args: A): Promise<R> => {
clearTimeout(lastTimeoutId)
return new Promise((resolve, reject) => {
const currentTimeoutId = setTimeout(async () => {
try {
if (currentTimeoutId === lastTimeoutId) {
const result = await fn(...args)
resolve(result)
}
} catch (err) {
reject(err)
}
}, wait)
lastTimeoutId = currentTimeoutId
})
}
}
/**
* Util to convert a Uint8Array to a string
* @param u8array
* @returns string
*/
export const convertUint8ToString: (u8array: Uint8Array) => string = u8array => {
const CHUNK_SZ = 0x8000
const c = []
for (let i = 0; i < u8array.length; i += CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, u8array.subarray(i, i + CHUNK_SZ)))
}
return c.join('')
}
/**
* Generate a new uuid
* @returns uuid
*/
export const uuid: () => string = () =>
// @ts-expect-error disable number[] + number warning
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c: number) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
)

View File

@ -1,3 +0,0 @@
export type Device = {
isMobile: boolean
}

View File

@ -1,8 +1,9 @@
import * as uint8arrays from 'uint8arrays'
import { get as getStore } from 'svelte/store'
import * as wn from 'webnative'
import { filesystemStore, galleryStore } from '../stores'
import { convertUint8ToString } from '$lib/common/utils'
import { addNotification } from '$lib/notifications'
import { filesystemStore, galleryStore } from '../stores'
export enum AREAS {
PUBLIC = 'Public',
@ -60,9 +61,7 @@ export const getImagesFromWNFS: () => Promise<void> = async () => {
const cid = isPrivate ? file.header.content.toString() : file.cid.toString()
// Create a base64 string to use as the image `src`
const src = `data:image/jpeg;base64, ${btoa(
convertUint8ToString(file.content as Uint8Array)
)}`
const src = `data:image/jpeg;base64, ${uint8arrays.toString(file.content, 'base64')}`
return {
cid,
@ -90,7 +89,6 @@ export const getImagesFromWNFS: () => Promise<void> = async () => {
loading: false,
}))
} catch (error) {
console.error(error)
galleryStore.update(store => ({
...store,
loading: false,
@ -132,12 +130,10 @@ export const uploadImageToWNFS: (
// Announce the changes to the server
await fs.publish()
console.log(`${image.name} image has been published`)
addNotification(`${image.name} image has been published`, 'success')
} catch (error) {
addNotification(error.message, 'error')
console.log(error)
}
}
@ -161,7 +157,6 @@ export const deleteImageFromWNFS: (name: string) => Promise<void> = async (name)
// Announce the changes to the server
await fs.publish()
console.log(`${name} image has been deleted`)
addNotification(`${name} image has been deleted`, 'success')
// Refetch images and update galleryStore
@ -171,7 +166,6 @@ export const deleteImageFromWNFS: (name: string) => Promise<void> = async (name)
}
} catch (error) {
addNotification(error.message, 'error')
console.error(error)
}
}

61
src/lib/init.ts Normal file
View File

@ -0,0 +1,61 @@
import * as webnative from 'webnative'
import { setup } from 'webnative'
import { filesystemStore, sessionStore } from '../stores'
import { getBackupStatus, type BackupStatus } from '$lib/auth/backup'
// TODO: Add a flag or script to turn debugging on/off
setup.debug({ enabled: false })
export const initialize = async (): Promise<void> => {
try {
let backupStatus: BackupStatus = null
const state: webnative.AppState = await webnative.app({ useWnfs: true })
switch (state.scenario) {
case webnative.AppScenario.NotAuthed:
sessionStore.set({
username: '',
authed: false,
loading: false,
backupCreated: null
})
break
case webnative.AppScenario.Authed:
backupStatus = await getBackupStatus(state.fs)
sessionStore.set({
username: state.username,
authed: state.authenticated,
loading: false,
backupCreated: backupStatus.created
})
filesystemStore.set(state.fs)
break
default:
break
}
} catch (error) {
switch (error) {
case webnative.InitialisationError.InsecureContext:
sessionStore.update(session => ({
...session,
loading: false,
error: 'Insecure Context'
}))
break
case webnative.InitialisationError.UnsupportedBrowser:
sessionStore.update(session => ({
...session,
loading: false,
error: 'Unsupported Browser'
}))
break
}
}
}

View File

@ -1,5 +1,4 @@
import { notificationStore } from '../stores'
import { uuid } from '$lib/common/utils'
export type Notification = {
id?: string
@ -22,7 +21,7 @@ export const addNotification: (
timeout?: number
) => void = (msg, type = 'info', timeout = 5000) => {
// uuid for each notification
const id = uuid()
const id = crypto.randomUUID()
// adding new notifications to the bottom of the list so they stack from bottom to top
notificationStore.update(rest => [

35
src/lib/utils.ts Normal file
View File

@ -0,0 +1,35 @@
export function asyncDebounce<A extends unknown[], R>(
fn: (...args: A) => Promise<R>,
wait: number
): (...args: A) => Promise<R> {
let lastTimeoutId: ReturnType<typeof setTimeout> | undefined = undefined
return (...args: A): Promise<R> => {
clearTimeout(lastTimeoutId)
return new Promise((resolve, reject) => {
const currentTimeoutId = setTimeout(async () => {
try {
if (currentTimeoutId === lastTimeoutId) {
const result = await fn(...args)
resolve(result)
}
} catch (err) {
reject(err)
}
}, wait)
lastTimeoutId = currentTimeoutId
})
}
}
export const extractSearchParam = (url: URL, param: string): string | null => {
const val = url.searchParams.get(param)
// clear the param from the URL
url.searchParams.delete(param)
history.replaceState(null, document.title, url.toString())
return val
}

View File

@ -1,3 +1,7 @@
export type BackupView = 'backup' | 'are-you-sure'
export type ConnectView = 'connect' | 'open-connected-device'
export type ConnectView = 'connect' | 'open-connected-device'
export type DelegateAccountView = 'connect-backup-device' | 'delegate-account'
export type LinkDeviceView = 'link-device' | 'load-filesystem'

View File

@ -1,42 +1,24 @@
<script lang="ts">
import { onMount } from 'svelte'
import '../global.css'
import { appDescription, appName, appURL } from '$lib/app-info'
import { initialize } from '$lib/common/webnative'
import { deviceStore, sessionStore, theme } from '../stores'
import { errorToMessage, type Session } from '$lib/session'
import Toast from '$components/notifications/Toast.svelte'
import Notifications from '$components/notifications/Notifications.svelte'
import { addNotification } from '$lib/notifications'
import { appDescription, appImageURL, appName, appURL } from '$lib/app-info'
import { sessionStore, themeStore } from '../stores'
import { errorToMessage } from '$lib/session'
import { initialize } from '$lib/init'
import Header from '$components/Header.svelte'
import Notifications from '$components/notifications/Notifications.svelte'
let session: Session = null
sessionStore.subscribe(val => {
session = val
})
onMount(() => { setDevice() })
const setDevice = () => {
if (window.innerWidth <= 768) {
deviceStore.set({ isMobile: true })
} else {
deviceStore.set({ isMobile: false })
sessionStore.subscribe(session => {
if (session.error) {
const message = errorToMessage(session.error)
addNotification(message, 'error')
}
}
})
const init = async () => {
await initialize()
}
const clearNotification = () => {
sessionStore.update(session => ({
...session,
error: null
}))
}
init()
</script>
@ -51,14 +33,14 @@
<meta property="og:description" content={appDescription} />
<meta property="og:url" content={appURL} />
<meta property="og:type" content="website" />
<meta property="og:image" content="TODO" />
<meta property="og:image" content={appImageURL} />
<meta property="og:image:alt" content="WebNative Template" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:width" content="1250" />
<meta property="og:image:height" content="358" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={appName} />
<meta name="twitter:description" content={appDescription} />
<meta name="twitter:image" content="TODO" />
<meta name="twitter:image" content={appImageURL} />
<meta name="twitter:image:alt" content={appName} />
<!-- See https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs for description. -->
@ -68,19 +50,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
</svelte:head>
<svelte:window on:resize={setDevice} />
<div data-theme={$theme} class="min-h-screen">
<div data-theme={$themeStore} class="min-h-screen">
<Header />
<Notifications />
{#if session.error}
<Toast
kind="error"
message={errorToMessage(session.error)}
on:clear={clearNotification}
/>
{/if}
<slot />
</div>

View File

@ -1,15 +1,12 @@
<script lang="ts">
import { page } from '$app/stores'
import { extractSearchParam } from '$lib/utils'
import type { BackupView } from '$lib/views'
import Backup from '$components/auth/backup/Backup.svelte'
import AreYouSure from '$components/auth/backup/AreYouSure.svelte'
import Backup from '$components/auth/backup/Backup.svelte'
let url = $page.url
let view = url.searchParams.get('view') ?? 'backup'
// clear the params
url.searchParams.delete('view')
history.replaceState(null, document.title, url.toString())
let view = extractSearchParam($page.url, 'view') ?? 'backup'
const navigate = (event: CustomEvent<{ view: BackupView }>) => {
view = event.detail.view
@ -19,5 +16,5 @@
{#if view === 'backup'}
<Backup on:navigate={navigate} />
{:else if view === 'are-you-sure'}
<AreYouSure on:navigate={navigate} />
<AreYouSure />
{/if}

View File

@ -1,12 +1,11 @@
<script lang="ts">
import type { ConnectView } from '$lib/views'
import Connect from '$components/auth/connect/Connect.svelte'
import OpenConnectedDevice from '$components/auth/connect/OpenConnectedDevice.svelte'
import type { ConnectView } from '$lib/views'
let view: ConnectView = 'connect'
const navigate = (event: CustomEvent<{ view: ConnectView }>) => {
console.log(event.detail.view)
view = event.detail.view
}
</script>

View File

@ -1,5 +1,123 @@
<script lang="ts">
import DelegateAccount from '$components/auth/link/DelegateAccount.svelte'
import { goto } from '$app/navigation'
import { onDestroy, onMount } from 'svelte'
import QRCode from 'qrcode-svg'
import { addNotification } from '$lib/notifications'
import { createAccountLinkingProducer } from '$lib/auth/linking'
import { filesystemStore, sessionStore, themeStore } from '../stores'
import { getBackupStatus, setBackupStatus } from '$lib/auth/backup'
import ConnectBackupDevice from '$components/auth/delegate-account/ConnectBackupDevice.svelte'
import DelegateAccount from '$components/auth/delegate-account/DelegateAccount.svelte'
import type { DelegateAccountView } from '$lib/views'
let view: DelegateAccountView = 'connect-backup-device'
let connectionLink = null
let qrcode = null
let fs = null
let backupCreated = true
let pin: number[]
let pinInput = ''
let pinError = false
let confirmPin = () => {}
let rejectPin = () => {}
let unsubscribeFilesystemStore = () => {}
let unsubscribeSessionStore = () => {}
onMount(() => {
unsubscribeFilesystemStore = filesystemStore.subscribe(async val => {
fs = val
if (fs) {
const backupStatus = await getBackupStatus(fs)
backupCreated = backupStatus.created
}
})
unsubscribeSessionStore = sessionStore.subscribe(async val => {
const username = val.username
if (username) {
const origin = window.location.origin
connectionLink = `${origin}/link-device?username=${username}`
qrcode = new QRCode({
content: connectionLink,
color: $themeStore === 'light' ? '#334155' : '#E2E8F0',
background: '#ffffff00'
}).svg()
initAccountLinkingProducer(username)
}
})
})
const initAccountLinkingProducer = async (username: string) => {
const accountLinkingProducer = await createAccountLinkingProducer(username)
accountLinkingProducer.on('challenge', detail => {
pin = detail.pin
confirmPin = detail.confirmPin
rejectPin = detail.rejectPin
view = 'delegate-account'
})
accountLinkingProducer.on('link', async ({ approved }) => {
if (approved) {
sessionStore.update(session => ({
...session,
backupCreated: true
}))
if (fs) {
await setBackupStatus(fs, { created: true })
addNotification("You've connected a backup device!", 'success')
goto('/')
} else {
addNotification(
'Missing filesystem. Unable to create a backup device.',
'error'
)
}
}
})
}
const checkPin = () => {
if (pin.join('') === pinInput) {
confirmPin()
} else {
pinError = true
}
}
const cancelConnection = () => {
rejectPin()
addNotification('The connection attempt was cancelled', 'info')
goto('/')
}
onDestroy(() => {
unsubscribeFilesystemStore()
unsubscribeSessionStore()
})
</script>
<DelegateAccount />
{#if view === 'connect-backup-device'}
<ConnectBackupDevice {qrcode} {connectionLink} {backupCreated} />
{:else if view === 'delegate-account'}
<DelegateAccount
bind:pinInput
bind:pinError
on:cancel={cancelConnection}
on:checkpin={checkPin}
/>
{/if}

View File

@ -1,10 +1,11 @@
<script lang="ts">
import { onDestroy } from 'svelte'
import { goto } from '$app/navigation'
import { galleryStore, sessionStore, theme as themeStore } from '../stores'
import { onDestroy } from 'svelte'
import { galleryStore, sessionStore, themeStore } from '../stores'
import { AREAS } from '$lib/gallery'
import Dropzone from '$components/gallery/upload/Dropzone.svelte'
import ImageGallery from '$components/gallery/imageGallery/ImageGallery.svelte'
import { AREAS } from '$lib/gallery'
/**
* Tab between the public/private areas and load associated images

View File

@ -2,8 +2,8 @@
import { goto } from '$app/navigation'
import { onDestroy } from 'svelte'
import { sessionStore } from '../stores'
import { appName } from '$lib/app-info'
import { sessionStore } from '../stores'
import type { Session } from '$lib/session'
import Shield from '$components/icons/Shield.svelte'

View File

@ -1,5 +1,60 @@
<script lang="ts">
import LinkDevice from '$components/auth/link/LinkDevice.svelte'
import type { account } from 'webnative'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { addNotification } from '$lib/notifications'
import { createAccountLinkingConsumer } from '$lib/auth/linking'
import { loadAccount } from '$lib/auth/account'
import type { LinkDeviceView } from '$lib/views'
import FilesystemActivity from '$components/common/FilesystemActivity.svelte'
import LinkDevice from '$components/auth/link-device/LinkDevice.svelte'
import { extractSearchParam } from '$lib/utils'
let view: LinkDeviceView = 'link-device'
let accountLinkingConsumer: account.AccountLinkingConsumer
let displayPin: string = ''
const username = extractSearchParam($page.url, 'username')
const initAccountLinkingConsumer = async () => {
accountLinkingConsumer = await createAccountLinkingConsumer(username)
accountLinkingConsumer.on('challenge', ({ pin }) => {
displayPin = pin.join('')
})
accountLinkingConsumer.on('link', async ({ approved, username }) => {
if (approved) {
view = 'load-filesystem'
await loadAccount(username)
addNotification("You're now connected!", 'success')
goto('/')
} else {
addNotification('The connection attempt was cancelled', 'info')
goto('/')
}
})
}
const cancelConnection = async () => {
addNotification('The connection attempt was cancelled', 'info')
await accountLinkingConsumer?.cancel()
goto('/')
}
initAccountLinkingConsumer()
</script>
<LinkDevice />
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
{#if view === 'link-device'}
<LinkDevice pin={displayPin} on:cancel={cancelConnection} />
{:else if view === 'load-filesystem'}
<FilesystemActivity activity="Loading" />
{/if}

View File

@ -3,14 +3,13 @@ import type { Writable } from 'svelte/store'
import type FileSystem from 'webnative/fs/index'
import { loadTheme } from '$lib/theme'
import type { Device } from '$lib/device'
import { AREAS } from '$lib/gallery'
import type { Gallery } from '$lib/gallery'
import type { Notification } from '$lib/notifications'
import type { Session } from '$lib/session'
import type { Theme } from '$lib/theme'
export const theme: Writable<Theme> = writable(loadTheme())
export const themeStore: Writable<Theme> = writable(loadTheme())
export const sessionStore: Writable<Session> = writable({
username: null,
@ -21,8 +20,6 @@ export const sessionStore: Writable<Session> = writable({
export const filesystemStore: Writable<FileSystem | null> = writable(null)
export const deviceStore: Writable<Device> = writable({ isMobile: true })
export const galleryStore: Writable<Gallery> = writable({
loading: true,
publicImages: [],

BIN
static/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -49,4 +49,9 @@ module.exports = {
rtl: false,
darkTheme: "dark",
},
purge: {
options: {
safelist: ['alert-success', 'alert-error', 'alert-info', 'alert-warning']
}
}
};