Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
radicle-desktop src views auth CreateIdentity.svelte
<script lang="ts">
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";

  import debounce from "lodash/debounce";

  import { invoke } from "@app/lib/invoke";
  import * as router from "@app/lib/router";
  import { createEventEmittersOnce } from "@app/lib/startup.svelte";

  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import TextInput from "@app/components/TextInput.svelte";
  import Layout from "@app/views/auth/Layout.svelte";

  let passphrase = $state("");
  let notMatchingPassphrases = $state<boolean>();
  let passphraseRepeat = $state("");
  let alias = $state("");
  const errors: { alias: ErrorWrapper[]; passphrase: ErrorWrapper[] } = {
    alias: [],
    passphrase: [],
  };

  const validatePassphraseRepeat = debounce(() => {
    if (passphrase !== passphraseRepeat && passphraseRepeat.length !== 0) {
      notMatchingPassphrases = true;
    }
  }, 400);

  function validateInput(field: "alias" | "passphrase") {
    if (field === "alias" && alias.length === 0) {
      errors.alias.push({ code: "AliasError.EmptyAlias" });
    }
    if (field === "alias" && alias.length > 32) {
      errors.alias.push({ code: "AliasError.TooLongAlias" });
    }
    if (field === "alias" && alias.includes(" ")) {
      errors.alias.push({ code: "AliasError.InvalidAlias" });
    }
    if (field === "passphrase" && passphrase.length === 0) {
      errors.passphrase.push({ code: "PassphraseError.InvalidPassphrase" });
    }
  }

  const validAlias = $derived(
    alias.length > 0 && alias.length <= 32 && !alias.includes(" "),
  );
  const validPassphrase = $derived(
    passphrase.length > 0 && passphrase === passphraseRepeat,
  );

  async function handleKeydown() {
    if (passphrase !== passphraseRepeat) {
      notMatchingPassphrases = true;

      return;
    }
    try {
      await invoke("init", { passphrase, alias });
      await invoke("startup");
      await invoke("authenticate", { passphrase });
      // Clearing the passphrases from memory.
      passphrase = "";
      passphraseRepeat = "";

      if (window.__TAURI_INTERNALS__) {
        await createEventEmittersOnce();
      }

      await router.push({ resource: "guide" });
    } catch (err) {
      const e = err as ErrorWrapper;
      if (e.code.startsWith("AliasError")) {
        errors.alias = [e];
      } else if (e.code.startsWith("PassphraseError")) {
        errors.passphrase = [e];
      }
      console.error(err);
    }

    return;
  }
</script>

<style>
  .header {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
    align-items: center;
    text-align: center;
  }
  .form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }
  .field {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
  }
  .label {
    font: var(--txt-body-s-semibold);
    color: var(--color-text-secondary);
  }
  .hint {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0 0 0.125rem;
  }
</style>

<Layout>
  <div class="header">
    <div class="txt-heading-l">Create identity</div>
    <div class="txt-body-m-regular" style:color="var(--color-text-secondary)">
      Set up your Radicle identity to get started.
    </div>
  </div>

  <div class="form">
    <div class="field">
      <div class="label">Alias</div>
      <TextInput
        autofocus
        onSubmit={handleKeydown}
        oninput={() => {
          errors.alias = [];
          if (alias.length > 0) {
            validateInput("alias");
          }
        }}
        placeholder="Enter desired alias"
        type="text"
        bind:value={alias} />
      {#if errors.alias.some(e => e.code.startsWith("AliasError"))}
        {#each errors.alias as error}
          <div
            style:color="var(--color-feedback-error-text)"
            class="hint txt-body-s-regular">
            <Icon name="warning" />
            {#if error.code === "AliasError.EmptyAlias"}
              <span>Alias cannot be empty.</span>
            {:else if error.code === "AliasError.TooLongAlias"}
              <span>Alias is too long, max 32 characters.</span>
            {:else if error.code === "AliasError.InvalidAlias"}
              <span>Alias cannot contain whitespace.</span>
            {/if}
          </div>
        {/each}
      {:else}
        <div class="hint txt-body-s-regular txt-missing">
          <Icon name="guide" /> Max 32 characters, no whitespace.
        </div>
      {/if}
    </div>

    <div class="field">
      <div class="label">Passphrase</div>
      <TextInput
        onSubmit={handleKeydown}
        oninput={() => {
          errors.passphrase = [];
          notMatchingPassphrases = false;
          if (passphrase.length > 0) {
            validateInput("passphrase");
          }
        }}
        placeholder="Enter passphrase to protect your keys"
        type="password"
        bind:value={passphrase} />
      {#if errors.passphrase.some(e => e.code.startsWith("PassphraseError"))}
        {#each errors.passphrase as error}
          <div
            style:color="var(--color-feedback-error-text)"
            class="hint txt-body-s-regular">
            <Icon name="warning" />
            {#if error.code === "PassphraseError.InvalidPassphrase"}
              <span>Passphrase cannot be empty.</span>
            {:else}
              <span>{error.message}</span>
            {/if}
          </div>
        {/each}
      {/if}
    </div>

    <div class="field">
      <TextInput
        onSubmit={handleKeydown}
        oninput={() => {
          errors.passphrase = [];
          notMatchingPassphrases = false;
          validatePassphraseRepeat();
        }}
        placeholder="Repeat passphrase"
        type="password"
        bind:value={passphraseRepeat} />
      {#if notMatchingPassphrases}
        <div
          style:color="var(--color-feedback-error-text)"
          class="hint txt-body-s-regular">
          <Icon name="warning" /> Passphrases don't match.
        </div>
      {/if}
    </div>

    <Button
      disabled={!(validAlias && validPassphrase)}
      variant="secondary"
      onclick={handleKeydown}
      styleWidth="100%">
      <Icon name="seed" />Create new identity
    </Button>
  </div>
</Layout>