Add create account flow

This commit is contained in:
Bojan Mojsilovic 2023-10-12 17:45:51 +02:00
parent bba3e08ce8
commit 13de3e001a
7 changed files with 1073 additions and 7 deletions

View File

@ -409,6 +409,10 @@ body {
}
.invisible {
display: none;
}
// Scrollbars
/* width */

View File

@ -9,6 +9,8 @@ import { createSignal } from 'solid-js';
export const [currentPin, setCurrentPin] = createSignal('');
export const [tempNsec, setTempNsec] = createSignal<string | undefined>();
export const generateKeys = (forceNewKey?: boolean) => {
const sec = forceNewKey ?
generatePrivateKey() :
@ -79,11 +81,14 @@ export const decryptWithPin = async (pin: string, cipher: string) => {
export const PrimalNostr: (pk?: string) => NostrExtension = (pk?: string) => {
const getSec = async () => {
let sec: string = pk || readSecFromStorage() || generatePrivateKey();
let sec: string = pk || readSecFromStorage() || tempNsec() || generatePrivateKey();
if (sec.startsWith(pinEncodePrefix)) {
sec = await decryptWithPin(currentPin(), sec);
}
console.log('SEC: ', sec)
const decoded = nip19.decode(sec);
if (decoded.type !== 'nsec' || !decoded.data) {

View File

@ -3,6 +3,9 @@ import { nip19 } from "nostr-tools"
export const hexToNpub = (hex: string | undefined): string => {
return hex ? nip19.npubEncode(hex) : '';
}
export const hexToNsec = (hex: string | undefined): string => {
return hex ? nip19.nsecEncode(hex) : '';
}
export const npubToHex = (npub: string | undefined): string => {
try {

View File

@ -325,3 +325,8 @@ export const sendAllowList = async (allowlist: string[], date: number, content:
return await sendEvent(event, relays, relaySettings);
};
export const getSuggestions = async () => {
const resp = await fetch('https://media.primal.net/api/suggestions');
console.log('>>>> ', resp)
};

View File

@ -0,0 +1,447 @@
.container {
// background-color: var(--background-card);
min-height: 100vh;
padding-bottom: 20px;
}
.fullHeader {
position: relative;
// background-color: var(--background-card);
padding-bottom: 20px;
}
.bannerPlaceholder {
position: relative;
width: 100%;
height: 214px;
}
.banner {
position: relative;
width: 100%;
height: 214px;
.uploadingOverlay {
position: absolute;
width: 100%;
height: 214px;
background-color: var(--background-card);
z-index: 1;
opacity: 0.6;
}
>label, .bannerPlaceholder>label {
position: relative;
height: 100%;
cursor: pointer;
>img {
width: 100%;
height: 214px;
object-fit: cover;
}
>div {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background-color: var(--background-card);
color: var(--text-primary);
opacity: 0;
transition: opacity 0.25s ease-out;
}
&:hover {
>div {
opacity: 0.8;
transition: opacity 0.25s ease-in;
}
}
}
}
.userImage {
position: absolute;
top: 148px;
left: 15px;
z-index: 2;
.avatar {
border: solid 4px var(--background-card);
border-radius: 50%;
background-color: var(--background-card);
.uploadingOverlay {
position: absolute;
top: 0;
left: 0;
width: 150px;
height: 150px;
border-radius: 50%;
background-color: var(--background-card);
z-index: 3;
opacity: 0.6;
}
}
}
.phoneAvatar {
display: none;
}
.desktopAvatar {
position: relative;
display: block;
.uploadAction {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
justify-content: center;
align-items: center;
background-color: var(--background-card);
color: var(--text-primary);
font-size: 18px;
font-weight: 400;
line-height: 20px;
cursor: pointer;
opacity: 0;
transition: opacity 0.25s ease-out;
}
&:hover {
.uploadAction {
opacity: 0.8;
transition: opacity 0.25s ease-in;
}
}
}
.blankActions {
height: 81px;
}
.uploadActions {
display: flex;
justify-content: flex-end;
.separator {
width: 1px;
height: 20px;
margin-block: 28px;
background-color: var(--subtile-devider);
}
.uploadButton {
background: none;
border: none;
outline: none;
color: var(--accent-1);
width: auto;
padding: 0;
margin-block: 28px;
margin-inline: 10px;
font-size: 16px;
font-weight: 400;
line-height: 20px;
text-transform: lowercase;
>label {
cursor: pointer;
}
}
}
form {
.inputLabel {
display: flex;
justify-content: space-between;
align-items: center;
label {
color: var(--text-secondary);
font-size: 16px;
font-weight: 400;
line-height: 22px;
text-transform: uppercase;
margin: 0;
}
.required {
display: flex;
justify-content: flex-end;
align-items: center;
font-size: 16px;
font-weight: 400;
line-height: 22px;
color: var(--text-tertiary-2);
.star {
color: var(--warning-color);
font-weight: 600;
margin-right: 2PX;
}
}
}
input, textarea {
border: none;
border-radius: 8px;
background: var(--background-card);
color: var(--text-primary);
font-size: 18px;
font-weight: 400;
line-height: 20px;
padding-block: 14px;
padding-inline: 19px;
width: 100%;
margin-bottom: 16px;
&:focus {
outline: none;
border: none;
box-shadow: none;
}
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--text-tertiary-2);
opacity: 1; /* Firefox */
}
&:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: var(--text-tertiary-2);
}
&::-ms-input-placeholder { /* Microsoft Edge */
color: var(--text-tertiary-2);
}
}
.inputWithPrefix {
display: flex;
align-items: center;
margin-bottom: var(--spacing);
.inputPrefix {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 400;
line-height: 20px;
height: 59px;
color: var(--text-tertiary-2);
padding-left: 19px;
border-radius: 8px 0px 0px 8px;
background: var(--background-card);
}
>input {
margin-bottom: 0;
padding-left: 8px;
border-radius: 0px 8px 8px 0px;
}
}
.inputError {
color: var(--warning-color);
font-size: 14px;
font-weight: 400;
line-height: 14px;
margin-top: calc(var(--spacing) * (-1));
margin-bottom: 6px;
}
}
@media only screen and (max-width: 720px) {
.banner {
width: 100%;
height: 125px;
>label {
>img {
width: 100%;
height: 125px;
object-fit: cover;
}
}
}
.userImage {
position: absolute;
top: 96px;
left: 15px;
}
.phoneAvatar {
display: block;
position: relative;
.uploadAction {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
justify-content: center;
align-items: center;
background-color: var(--background-card);
color: var(--text-primary);
text-align: center;
font-size: 18px;
font-weight: 400;
line-height: 20px;
cursor: pointer;
opacity: 0;
transition: opacity 0.25s ease-out;
}
&:hover {
.uploadAction {
opacity: 0.8;
transition: opacity 0.25s ease-in;
}
}
}
.desktopAvatar {
display: none;
}
}
.cacheFlag {
img {
border: 1px solid red;
}
}
.horizontalSeparator {
width: 100%;
height: 1px;
margin-block: 16px;
background-color: var(--subtile-devider);
}
.moreTrigger {
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--subtile-devider);
padding-block: 16px;
button {
background: none;
border: none;
outline: none;
color: var(--accent-1);
width: auto;
padding: 0;
font-size: 16px;
font-weight: 400;
line-height: 20px;
text-transform: capitalize;
margin: 0;
&.hidden {
&::after {
border-style: solid;
border-width: 0.1em 0.1em 0 0;
content: '';
display: inline-block;
height: 6px;
width: 6px;
margin-top: 4px;
margin-left: 4px;
position: relative;
transform: rotate(-220deg);
vertical-align: top;
}
}
&.shown {
&::after {
border-style: solid;
border-width: 0.1em 0.1em 0 0;
content: '';
display: inline-block;
height: 6px;
width: 6px;
margin-top: 8px;
margin-left: 4px;
position: relative;
transform: rotate(-45deg);
vertical-align: top;
}
}
}
}
.moreInputs {
overflow: hidden;
&.hide {
max-height: 0;
transition: max-height 0.25s ease-out;
}
&.show {
max-height: 210px;
transition: max-height 0.25s ease-in;
}
}
.formSubmit {
display: flex;
justify-content: flex-start;
button {
width: 80px;
height: 28px;
margin: 0px 16px 11px 0px;
&.primaryButton {
border: none;
border-radius: 6px;
padding: 0px;
font-size: 14px;
line-height: 20px;
font-weight: 700;
background: var(--brand-gradient-vertical);
color: var(--text-primary);
>span {
opacity: 0.75;
}
}
&.secondaryButton {
border: none;
border-radius: 6px;
padding: 1px;
font-size: 14px;
line-height: 20px;
font-weight: 700;
background: var(--brand-gradient-vertical);
color: var(--text-tertiary-2);
>div {
width: 100%;
height: 100%;
vertical-align: middle;
border-radius: 6px;
background-color: var(--background-card);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
}
}

View File

@ -1,12 +1,584 @@
import { Component } from 'solid-js';
import MissingPage from '../components/MissingPage/MissingPage';
import { useIntl } from '@cookbook/solid-intl';
import { useNavigate } from '@solidjs/router';
import { Component, createEffect, createSignal, onMount, Show } from 'solid-js';
import { APP_ID } from '../App';
import Avatar from '../components/Avatar/Avatar';
import Loader from '../components/Loader/Loader';
import PageCaption from '../components/PageCaption/PageCaption';
import PageTitle from '../components/PageTitle/PageTitle';
import { useToastContext } from '../components/Toaster/Toaster';
import { usernameRegex, Kind } from '../constants';
import { useAccountContext } from '../contexts/AccountContext';
import { useMediaContext } from '../contexts/MediaContext';
import { useProfileContext } from '../contexts/ProfileContext';
import { uploadMedia } from '../lib/media';
import { getSuggestions, sendProfile } from '../lib/profile';
import { subscribeTo as uploadSub } from "../uploadSocket";
import {
actions as tActions,
account as tAccount,
settings as tSettings,
toast as tToast,
} from '../translations';
import { NostrMediaUploaded, NostrRelays } from '../types/primal';
import styles from './CreateAccount.module.scss';
import { createStore, reconcile } from 'solid-js/store';
import { generateKeys, setTempNsec } from '../lib/PrimalNostr';
import { hexToNpub, hexToNsec } from '../lib/keys';
import { storeSec } from '../lib/localStore';
import { getPreConfiguredRelays } from '../lib/relays';
import CreatePinModal from '../components/CreatePinModal/CreatePinModal';
type AutoSizedTextArea = HTMLTextAreaElement & { _baseScrollHeight: number };
const CreateAccount: Component = () => { const intl = useIntl();
const profile = useProfileContext();
const media = useMediaContext();
const account = useAccountContext();
const toast = useToastContext();
const navigate = useNavigate();
let textArea: HTMLTextAreaElement | undefined;
let fileUploadAvatar: HTMLInputElement | undefined;
let fileUploadBanner: HTMLInputElement | undefined;
let nameInput: HTMLInputElement | undefined;
const [isBannerCached, setIsBannerCached] = createSignal(false);
const [isMoreVisible, setIsMoreVisible] = createSignal(false);
const [isUploadingAvatar, setIsUploadingAvatar] = createSignal(false);
const [isUploadingBanner, setIsUploadingBanner] = createSignal(false);
const [avatarPreview, setAvatarPreview] = createSignal<string>();
const [bannerPreview, setBannerPreview] = createSignal<string>();
const [isNameValid, setIsNameValid] = createSignal<boolean>(false);
const flagBannerForWarning = () => {
const dev = localStorage.getItem('devMode') === 'true';
// @ts-ignore
if (isBannerCached() || !dev) {
return '';
}
return styles.cacheFlag;
}
const imgError = (event: any) => {
const banner = document.getElementById('profile_banner');
if (banner) {
banner.innerHTML = `<div class="${styles.bannerPlaceholder}"></div>`;
}
return true;
}
const banner = () => {
const src = bannerPreview();
const url = media?.actions.getMediaUrl(src, 'm', true);
setIsBannerCached(!!url);
return url ?? src;
}
const setProfile = (hex: string | undefined) => {
profile?.actions.setProfileKey(hex);
profile?.actions.clearNotes();
profile?.actions.fetchNotes(hex);
}
const getScrollHeight = (elm: AutoSizedTextArea) => {
var savedValue = elm.value
elm.value = ''
elm._baseScrollHeight = elm.scrollHeight
elm.value = savedValue
}
const onExpandableTextareaInput: () => void = () => {
const maxHeight = document.documentElement.clientHeight || window.innerHeight || 0;
const elm = textArea as AutoSizedTextArea;
const minRows = parseInt(elm.getAttribute('data-min-rows') || '0');
!elm._baseScrollHeight && getScrollHeight(elm);
if (elm.scrollHeight >= (maxHeight / 3)) {
return;
}
elm.rows = minRows;
const rows = Math.ceil((elm.scrollHeight - elm._baseScrollHeight) / 20);
elm.rows = minRows + rows;
}
const onNameInput = () => {
const value = nameInput?.value || '';
setIsNameValid(usernameRegex.test(value))
};
const onUpload = (target: 'picture' | 'banner', fileUpload: HTMLInputElement | undefined) => {
if (!fileUpload) {
return;
}
const file = fileUpload.files ? fileUpload.files[0] : null;
// @ts-ignore fileUpload.value assignment
file && uploadFile(file, target, () => fileUpload.value = null);
}
const uploadFile = (file: File, target: 'picture' | 'banner', callback?: () => void) => {
target === 'banner' && setIsUploadingBanner(true);
target === 'picture' && setIsUploadingAvatar(true);
const reader = new FileReader();
reader.onload = (e) => {
if (!e.target?.result) {
return;
}
const subid = `upload_${APP_ID}`;
const data = e.target?.result as string;
const unsub = uploadSub(subid, (type, subId, content) => {
if (type === 'EVENT') {
if (!content) {
return;
}
if (content.kind === Kind.Uploaded) {
const uploaded = content as NostrMediaUploaded;
if (target === 'picture') {
setAvatarPreview(uploaded.content);
return
}
if (target === 'banner') {
setBannerPreview(uploaded.content);
return
}
return;
}
}
if (type === 'NOTICE') {
target === 'banner' && setIsUploadingBanner(false);
target === 'picture' && setIsUploadingAvatar(false);
unsub();
return;
}
if (type === 'EOSE') {
target === 'banner' && setIsUploadingBanner(false);
target === 'picture' && setIsUploadingAvatar(false);
unsub();
return;
}
});
uploadMedia(createdAccount.pubkey, subid, data);
}
reader.readAsDataURL(file);
callback && callback();
}
const onSubmit = async (e: SubmitEvent) => {
e.preventDefault();
if (!e.target || !account) {
return false;
}
const data = new FormData(e.target as HTMLFormElement);
const name = data.get('name')?.toString() || '';
if (!usernameRegex.test(name)) {
toast?.sendWarning(intl.formatMessage(tSettings.profile.name.formError));
return false;
}
let relaySettings = getPreConfiguredRelays();
let metadata: Record<string, string> = {};
[ 'displayName',
'name',
'website',
'about',
'lud16',
'nip05',
'picture',
'banner',
].forEach(key => {
if (data.get(key)) {
metadata[key] = data.get(key) as string;
if (key === 'displayName') {
metadata['display_name'] = data.get(key) as string;
}
}
});
const CreateAccount: Component = () => {
const { success } = await sendProfile({ ...metadata }, account.relays, relaySettings);
if (success) {
toast?.sendSuccess(intl.formatMessage(tToast.updateProfileSuccess));
setShowCreatePin(true);
return false;
}
toast?.sendWarning(intl.formatMessage(tToast.updateProfileFail))
return false;
};
const [createdAccount, setCreatedAccount] = createStore<{ sec?: string, pubkey?: string, relays?: NostrRelays }>({});
const [currentStep, setCurrentStep] = createSignal<'name' | 'info' | 'follow'>('name');
const [showCreatePin, setShowCreatePin] = createSignal(false);
const toNext = () => {
switch(currentStep()) {
case 'name':
setCurrentStep('info');
break;
case 'info':
setCurrentStep('follow');
break;
default:
break;
}
};
const toPrevious = () => {
switch(currentStep()) {
case 'info':
setCurrentStep('name');
break;
case 'follow':
setCurrentStep('info');
break;
default:
break;
}
};
onMount(() => {
const { sec, pubkey } = generateKeys(true);
const nsec = hexToNsec(sec);
account?.actions.setSec(nsec);
setTempNsec(nsec);
setCreatedAccount(() => ({ sec: nsec, pubkey }));
getSuggestions()
});
const onStoreSec = (sec: string | undefined) => {
storeSec(sec);
setTempNsec(undefined);
setCreatedAccount(reconcile({}));
onAbort();
navigate('/');
}
const onAbort = () => {
setShowCreatePin(false);
}
return (
<>
<MissingPage title="create account" />
</>
<div class={styles.container}>
<PageTitle title={intl.formatMessage(tAccount.create.title)} />
<PageCaption title={intl.formatMessage(tAccount.create.title)} />
<div class={['name', 'info'].includes(currentStep()) ? '' : 'invisible'}>
<div id="central_header" class={styles.fullHeader}>
<div id="profile_banner" class={`${styles.banner} ${flagBannerForWarning()}`}>
<Show when={isUploadingBanner()}>
<div class={styles.uploadingOverlay}><Loader /></div>
</Show>
<Show
when={banner()}
fallback={
<div class={styles.bannerPlaceholder}>
<label for="upload-banner">
<div>{intl.formatMessage(tSettings.profile.uploadBanner)}</div>
</label>
</div>
}
>
<label for="upload-banner">
<img
src={banner()}
onerror={imgError}
/>
<div>{intl.formatMessage(tSettings.profile.uploadBanner)}</div>
</label>
</Show>
</div>
<div class={styles.userImage}>
<div class={styles.avatar}>
<Show when={isUploadingAvatar()}>
<div class={styles.uploadingOverlay}><Loader /></div>
</Show>
<label for="upload-avatar">
<div class={styles.desktopAvatar}>
<Avatar src={avatarPreview()} size="xxl" />
<div class={styles.uploadAction}>
{intl.formatMessage(tSettings.profile.uploadAvatar)}
</div>
</div>
<div class={styles.phoneAvatar}>
<Avatar src={avatarPreview()} size="lg" />
<div class={styles.uploadAction}>
{intl.formatMessage(tSettings.profile.uploadAvatar)}
</div>
</div>
</label>
</div>
</div>
<Show
when={currentStep() === 'name'}
fallback={
<div class={styles.blankActions}></div>
}
>
<div class={styles.uploadActions}>
<div class={styles.uploadButton}>
<input
id="upload-avatar"
type="file"
onChange={() => onUpload('picture', fileUploadAvatar)}
ref={fileUploadAvatar}
hidden={true}
accept="image/*"
/>
<label for="upload-avatar">
{intl.formatMessage(tSettings.profile.uploadAvatar)}
</label>
</div>
<div class={styles.separator}></div>
<div class={styles.uploadButton}>
<input
id="upload-banner"
type="file"
onchange={() => onUpload('banner', fileUploadBanner)}
ref={fileUploadBanner}
hidden={true}
accept="image/*"
/>
<label for="upload-banner">
{intl.formatMessage(tSettings.profile.uploadBanner)}
</label>
</div>
</div>
</Show>
</div>
</div>
<form onSubmit={onSubmit}>
<div class={currentStep() === 'name' ? '' : 'invisible'}>
<div class={styles.inputLabel}>
<label for='name'>{intl.formatMessage(tSettings.profile.name.label)}</label>
<span class={styles.required}>
<span class={styles.star}>*</span>
{intl.formatMessage(tSettings.profile.required)}
</span>
</div>
<div class={styles.inputWithPrefix}>
<div class={styles.inputPrefix}>
@
</div>
<input
name='name'
type='text'
ref={nameInput}
class={styles.inputWithPrefix}
placeholder={intl.formatMessage(tSettings.profile.name.placeholder)}
value={profile?.userProfile?.name || ''}
onInput={onNameInput}
/>
</div>
<Show when={!isNameValid()}>
<div class={styles.inputError}>
{intl.formatMessage(tSettings.profile.name.error)}
</div>
</Show>
<div class={styles.inputLabel}>
<label for='displayName'>{intl.formatMessage(tSettings.profile.displayName.label)}</label>
</div>
<input
name='displayName'
type='text'
placeholder={intl.formatMessage(tSettings.profile.displayName.placeholder)}
value={profile?.userProfile?.displayName || profile?.userProfile?.display_name || ''}
/>
<div class={`${styles.moreInputs} ${isMoreVisible() ? styles.show : styles.hide}`}>
<div class={styles.inputLabel}>
<label for='picture'>{intl.formatMessage(tSettings.profile.picture.label)}</label>
</div>
<input
name='picture'
type='text'
placeholder={intl.formatMessage(tSettings.profile.picture.placeholder)}
value={avatarPreview() || ''}
onChange={(e: Event) => {
const target = e.target as HTMLInputElement;
target.value && setAvatarPreview(target.value);
}}
/>
<div class={styles.inputLabel}>
<label for='banner'>{intl.formatMessage(tSettings.profile.banner.label)}</label>
</div>
<input
name='banner'
type='text'
placeholder={intl.formatMessage(tSettings.profile.banner.placeholder)}
value={bannerPreview() || ''}
onChange={(e: Event) => {
const target = e.target as HTMLInputElement;
target.value && setBannerPreview(target.value);
}}
/>
</div>
</div>
<div class={currentStep() === 'info' ? '' : 'invisible'}>
<div class={styles.inputLabel}>
<label for='website'>{intl.formatMessage(tSettings.profile.website.label)}</label>
</div>
<input
name='website'
type='text'
placeholder={intl.formatMessage(tSettings.profile.website.placeholder)}
value={profile?.userProfile?.website || ''}
/>
<div class={styles.inputLabel}>
<label for='about'>{intl.formatMessage(tSettings.profile.about.label)}</label>
</div>
<textarea
name='about'
placeholder={intl.formatMessage(tSettings.profile.about.placeholder)}
value={profile?.userProfile?.about || ''}
ref={textArea}
rows={1}
data-min-rows={1}
onInput={onExpandableTextareaInput}
/>
<div class={styles.inputLabel}>
<label for='lud16'>{intl.formatMessage(tSettings.profile.lud16.label)}</label>
</div>
<input
name='lud16'
type='text'
placeholder={intl.formatMessage(tSettings.profile.lud16.placeholder)}
value={profile?.userProfile?.lud16 || ''}
/>
<div class={styles.inputLabel}>
<label for='nip05'>{intl.formatMessage(tSettings.profile.nip05.label)}</label>
</div>
<input
name='nip05'
type='text'
placeholder={intl.formatMessage(tSettings.profile.nip05.placeholder)}
value={profile?.userProfile?.nip05 || ''}
/>
</div>
<div class={currentStep() === 'follow' ? '' : 'invisible'}>
<div>
HERE BE FOLLOWS
</div>
</div>
<div class={styles.formSubmit}>
<Show when={currentStep() !== 'name'}>
<button
type='button'
class={styles.secondaryButton}
onClick={toPrevious}
>
<div>
<span>
{intl.formatMessage(tActions.previous)}
</span>
</div>
</button>
</Show>
<Show
when={currentStep() === 'follow'}
fallback={
<button
type='button'
class={styles.primaryButton}
disabled={currentStep() === 'name' && !isNameValid()}
onClick={toNext}
>
{intl.formatMessage(tActions.next)}
</button>
}
>
<button
type='submit'
class={styles.primaryButton}
disabled={!isNameValid()}
>
{intl.formatMessage(tActions.finish)}
</button>
</Show>
<button
type='button'
class={styles.secondaryButton}
onClick={() => navigate('/profile')}
>
<div>
<span>{intl.formatMessage(tActions.cancel)}</span>
</div>
</button>
</div>
</form>
<CreatePinModal
open={showCreatePin()}
onAbort={() => {
onStoreSec(createdAccount.sec);
}}
valueToEncrypt={createdAccount.sec}
onPinApplied={onStoreSec}
/>
</div>
);
}

View File

@ -24,6 +24,21 @@ export const account = {
defaultMessage: 'New to Nostr? Create your account now and join this magical place. Its quick and easy!',
description: 'Label inviting users to join Nostr',
},
create: {
title: {
id: 'settings.account.title',
defaultMessage: 'Create Account',
description: 'Title of the create account page',
},
descriptions: {
step_one: {
id: 'settings.account.descriptions.step_on',
defaultMessage: "Lets start with the basics. Only the username is required!",
description: 'Description on step one',
},
}
},
};
export const login = {
@ -188,6 +203,21 @@ export const actions = {
defaultMessage: 'save',
description: 'Save changes action label',
},
previous: {
id: 'actions.previous',
defaultMessage: 'previous',
description: 'Go to previous step action label',
},
next: {
id: 'actions.next',
defaultMessage: 'next',
description: 'Go to next step action label',
},
finish: {
id: 'actions.finish',
defaultMessage: 'finish',
description: 'Finish the wizard action label',
},
editProfile: {
id: 'actions.editProfile',
defaultMessage: 'edit profile',