Skip to content

Commit

Permalink
feat(wasm): Import/export account (#893)
Browse files Browse the repository at this point in the history
  • Loading branch information
Flemmli97 authored Dec 20, 2024
1 parent 0907378 commit 623ba64
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 15 deletions.
21 changes: 19 additions & 2 deletions src/lib/components/settings/OrderedPhrase.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import { fade } from "svelte/transition"
import { animationDelay, animationDuration } from "$lib/globals/animations"
import { Text, Loader } from "$lib/elements"
import Input from "$lib/elements/Input/Input.svelte"
import type { Writable } from "svelte/store"
export let number: number = 0
export let word: string = "UNKNOWN"
export let loading: boolean = false
export let editable: boolean = false
</script>

<div class="ordered-phrase">
Expand All @@ -18,12 +21,16 @@
</div>
{/if}
</span>
<span class="word">
<span class="word {editable ? 'editable' : ''}">
{#if loading}
<Loader text />
{:else}
<div data-cy="ordered-phrase-word-{number}" in:fade={{ duration: animationDuration, delay: number * animationDelay }}>
<Text>{word}</Text>
{#if editable}
<Input bind:value={word} noCapitalize on:paste></Input>
{:else}
<Text>{word}</Text>
{/if}
</div>
{/if}
</span>
Expand Down Expand Up @@ -64,6 +71,16 @@
display: inline-block;
flex: 1;
padding: var(--padding-minimal) var(--padding);
&.editable {
padding: 0;
}
}
:global(.input-group > .input-container) {
border: none;
}
:global(.input-group > .input-container::selection) {
border: none;
}
}
</style>
4 changes: 3 additions & 1 deletion src/lib/elements/Input/FileInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
export let hidden: boolean = false
export let clss: string = ""
export let allowed: string | undefined = undefined
export let multiple: boolean = true
const dispatch: EventDispatcher<Record<string, File[]>> = createEventDispatcher()
Expand All @@ -24,4 +26,4 @@
}
</script>

<input class={clss} style={hidden ? "display: none" : ""} multiple type="file" bind:this={refSelf} bind:files={fileInput} />
<input class={clss} style={hidden ? "display: none" : ""} multiple={multiple} accept={allowed ? allowed : "*"} type="file" bind:this={refSelf} bind:files={fileInput} />
6 changes: 5 additions & 1 deletion src/lib/elements/Input/Input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
export let rich: boolean = false
export let autoFocus: boolean = false
export let rules: InputRules = new InputRules()
export let noCapitalize: boolean = false
let errorMessage: string = ""
Expand Down Expand Up @@ -179,14 +180,17 @@
<input
class="input {centered ? 'centered' : ''} {disabled ? 'disabled' : ''}"
type="text"
autocapitalize={noCapitalize ? "none" : undefined}
autocorrect={noCapitalize ? "off" : undefined}
bind:this={$input}
on:focus={handleFocus}
bind:value={$writableValue}
placeholder={placeholder}
on:keydown={onKeyDown}
on:input={onInput}
on:blur={onBlur}
autofocus={isAndroidOriOS() ? isKeyboardOpened : autoFocus} />
autofocus={isAndroidOriOS() ? isKeyboardOpened : autoFocus}
on:paste />
</div>
</div>
{#if errorMessage}
Expand Down
18 changes: 18 additions & 0 deletions src/lib/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,16 @@
"new": "Create New Account",
"import": "Import Account",
"profile": "Create new profile"
},
"import": {
"title": "Import Account",
"description": "Import your account from remote or a backup file",
"passphrase": "Add passphrase",
"remote": "Import from remote",
"file": "Import from file",
"warning": "By continuing your old account will be overwritten!",
"fail": "Failed to import account",
"fail.null": "Failed to create identity during account import"
}
}
},
Expand Down Expand Up @@ -370,6 +380,14 @@
"remove": "Your recovery phrase will not be stored anymore and will be removed. Make sure to save the phrase! This change is irreversible!",
"remove.yes": "I understand",
"remove.no": "Cancel"
},
"export": {
"label": "Export",
"description": "Export your account manually to remote or a file",
"remote": "Remote",
"file": "File",
"fail": "Failed to export account",
"success": "Successfully exported account to remote"
}
},
"calling": {
Expand Down
8 changes: 3 additions & 5 deletions src/lib/layouts/login/Entrypoint.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,9 @@
<Button text={$_("pages.auth.create.new")} hook="button-create-account" on:click={_ => (page = LoginPage.Username)} appearance={Appearance.Primary} fill>
<Icon icon={Shape.Plus} />
</Button>
{#if get(SettingsStore.state).devmode}
<Button text={$_("pages.auth.create.import")} hook="button-import-account" on:click={_ => (showConfigureRelay = true)} appearance={Appearance.Alt} fill>
<Icon icon={Shape.ArrowUp} />
</Button>
{/if}
<Button text={$_("pages.auth.create.import")} hook="button-import-account" on:click={_ => (page = LoginPage.Import)} appearance={Appearance.Alt} fill>
<Icon icon={Shape.ArrowUp} />
</Button>
</Controls>
</div>

Expand Down
201 changes: 201 additions & 0 deletions src/lib/layouts/login/ImportAccount.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<script lang="ts">
import { Icon, Button, Title, Text } from "$lib/elements"
import { Appearance, Shape } from "$lib/enums"
import { _ } from "svelte-i18n"
import { Identity, WarpInstance } from "warp-wasm"
import { MultipassStoreInstance } from "$lib/wasm/MultipassStore"
import { OrderedPhrase } from "$lib/components"
import FileInput from "$lib/elements/Input/FileInput.svelte"
import { TesseractStoreInstance } from "$lib/wasm/TesseractStore"
import { RelayStore } from "$lib/state/wasm/relays"
import { WarpStore } from "$lib/wasm/WarpStore"
import { get } from "svelte/store"
import { LoginPage } from "."
import Unlock from "./Unlock.svelte"
import { AuthStore } from "$lib/state/auth"
import { Store } from "$lib/state/Store"
import { ToastMessage } from "$lib/state/ui/toast"
export let page: LoginPage
export let onImport: (identity: Identity) => void
let loading = false
let passphrase: string[] = new Array(12).fill("")
let passphraseUpload: FileInput
let accountUpload: FileInput
let failed: string = ""
let pin = ""
// We cache it here to not init the warp instances in all stores since its not ready at this time
let warp: WarpInstance
async function setupTesseract(pinInput: string) {
await TesseractStoreInstance.initTesseract()
let tesseract = await TesseractStoreInstance.getTesseract()
if (TesseractStoreInstance.exists()) {
tesseract.clear()
}
let result = await TesseractStoreInstance.unlock(pinInput)
let failed = false
result.onFailure(_ => {
failed = true
console.log("Failed to unlock tesseract")
})
if (failed) return
let addresses = Object.values(get(RelayStore.state))
.filter(r => r.active)
.map(r => r.address)
warp = await WarpStore.createIpfs(addresses)
pin = pinInput
}
async function readPassphrase(files: File[]) {
let file = files[0]
let content = await file.text()
// Allow splitting by new lines and/or whitespace
passphrase = parsePhrase(content)
}
function parsePhrase(phrase: string) {
return phrase.trim().replace(/\s\s+/g, " ").split(" ")
}
async function importAccount(files?: File[]) {
loading = true
let memory: Uint8Array | undefined = undefined
if (files) {
memory = new Uint8Array(await files[0].arrayBuffer())
}
let res = await MultipassStoreInstance.importAccount(passphrase.join(" "), { to: memory, multipassBox: warp.multipass })
res.onSuccess(identity => {
if (identity) {
AuthStore.setStoredPin(pin)
WarpStore.updateWarpInstance(warp)
onImport(identity)
} else {
Store.addToastNotification(new ToastMessage($_("pages.auth.import.fail.null"), "", 2))
}
})
res.onFailure(err => {
Store.addToastNotification(new ToastMessage($_("pages.auth.import.fail"), err, 2))
failed = err
})
loading = false
}
</script>

{#if pin.length == 0}
<Unlock
create={true}
importing={true}
on:pin={async e => {
await setupTesseract(e.detail.pin)
e.detail.done()
}} />
{:else}
<div class="account-import">
<div class="header">
<Title hook="title-import-account">{$_("pages.auth.import.title")}</Title>
<Text hook="text-import-account-secondary" muted>{$_("pages.auth.import.description")}</Text>
</div>
{#each passphrase as word, i}
<OrderedPhrase
number={i + 1}
bind:word={word}
editable
loading={loading}
on:paste={e => {
if (i == 0) {
passphrase = parsePhrase(e.clipboardData?.getData("Text") ?? "")
e.preventDefault()
e.stopPropagation()
}
}} />
{/each}
<Button
hook="upload-passphrase"
on:click={() => {
passphraseUpload.click()
}}
text={$_("pages.auth.import.passphrase")}>
<Icon icon={Shape.Details} />
</Button>
<div class="import-button-group">
<Button
hook="button-import-account-go-back"
loading={loading}
text={$_("controls.go_back")}
appearance={Appearance.Alt}
on:click={() => {
page = LoginPage.EntryPoint
TesseractStoreInstance.clearTesseract()
}}>
<Icon icon={Shape.ArrowLeft} />
</Button>
<Button
hook="import-account-file"
loading={loading}
disabled={passphrase.find(s => s.length === 0) !== undefined}
on:click={() => {
accountUpload.click()
}}
text={$_("pages.auth.import.file")}>
<Icon icon={Shape.Document} />
</Button>
<Button
hook="import-account"
loading={loading}
disabled={passphrase.find(s => s.length === 0) !== undefined}
on:click={() => {
importAccount()
}}
text={$_("pages.auth.import.remote")}>
<Icon icon={Shape.Globe} />
</Button>
<FileInput
bind:this={passphraseUpload}
hidden
allowed={"text/plain"}
on:select={e => {
readPassphrase(e.detail)
}} />
<FileInput
bind:this={accountUpload}
hidden
allowed={".upk"}
on:select={e => {
importAccount(e.detail)
}} />
</div>
</div>
{/if}

<style lang="scss">
.account-import {
align-self: center;
align-content: center;
justify-content: center;
display: inline-flex;
flex-direction: row;
gap: var(--gap);
padding: var(--padding);
max-width: var(--max-component-width);
flex-wrap: wrap;
flex: 1;
.import-button-group {
width: 100%;
display: flex;
justify-content: center;
@media (max-width: 600px) {
flex-direction: column;
align-items: center;
gap: var(--gap);
}
}
}
</style>
6 changes: 6 additions & 0 deletions src/lib/layouts/login/Unlock.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import { ToastMessage } from "$lib/state/ui/toast"
import { TesseractStoreInstance } from "$lib/wasm/TesseractStore"
export let create: boolean = false
export let importing: boolean = false
const dispatch = createEventDispatcher()
let loading = false
Expand Down Expand Up @@ -119,6 +121,10 @@
</Modal>
{/if}

{#if importing}
<Text appearance={Appearance.Warning}>{$_("pages.auth.import.warning")}</Text>
{/if}

{#if loading}
<Label text={$_("generic.loading")} />
{:else}
Expand Down
3 changes: 3 additions & 0 deletions src/lib/wasm/HandleWarpErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum WarpError {
CONSTELLATION_NOT_FOUND = "Constellation instance not found",
RAYGUN_NOT_FOUND = "Raygun instance not found",
FILE_SIZE_EXCEEDED = "File size exceeded",
INVALID_PHRASE = "Invalid word in phrase",
}

export function handleErrors(error: any): WarpError {
Expand All @@ -40,6 +41,8 @@ export function handleErrors(error: any): WarpError {
return WarpError.ITEM_ALREADY_EXIST_WITH_SAME_NAME
case message.includes(WarpError.ITEM_DOES_NOT_EXIST):
return WarpError.ITEM_DOES_NOT_EXIST
case message.includes(WarpError.INVALID_PHRASE.toLowerCase()):
return WarpError.INVALID_PHRASE
default:
return WarpError.GENERAL_ERROR
}
Expand Down
Loading

0 comments on commit 623ba64

Please sign in to comment.