Merge pull request #233 from v0l/onboarding

feat: onboarding
This commit is contained in:
2023-02-10 10:46:12 +00:00
committed by GitHub
38 changed files with 1192 additions and 209 deletions

View File

@ -14,7 +14,7 @@
.write-dm {
position: fixed;
bottom: 0;
background-color: var(--gray-light);
background-color: var(--gray);
width: inherit;
border-radius: 5px 5px 0 0;
}

View File

@ -7,24 +7,24 @@ import messages from "./messages";
import "./Verification.css";
export default function VerificationPage() {
const services = [
{
name: "Snort",
service: `${ApiHost}/api/v1/n5sp`,
link: "https://snort.social/",
supportLink: "https://snort.social/help",
about: <FormattedMessage {...messages.SnortSocialNip} />,
},
{
name: "Nostr Plebs",
service: "https://nostrplebs.com/api/v1",
link: "https://nostrplebs.com/",
supportLink: "https://nostrplebs.com/manage",
about: <FormattedMessage {...messages.NostrPlebsNip} />,
},
];
export const services = [
{
name: "Snort",
service: `${ApiHost}/api/v1/n5sp`,
link: "https://snort.social/",
supportLink: "https://snort.social/help",
about: <FormattedMessage {...messages.SnortSocialNip} />,
},
{
name: "Nostr Plebs",
service: "https://nostrplebs.com/api/v1",
link: "https://nostrplebs.com/",
supportLink: "https://nostrplebs.com/manage",
about: <FormattedMessage {...messages.NostrPlebsNip} />,
},
];
export default function VerificationPage() {
return (
<div className="main-content verification">
<h2>

View File

@ -1,21 +1,32 @@
import { useIntl, FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { RecommendedFollows } from "Const";
import FollowListBase from "Element/FollowListBase";
import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import messages from "./messages";
export default function DiscoverFollows() {
const navigate = useNavigate();
const sortedRecommends = useMemo(() => {
const { formatMessage } = useIntl();
const sortedReccomends = useMemo(() => {
return RecommendedFollows.sort(() => (Math.random() >= 0.5 ? -1 : 1));
}, []);
return (
<>
<h2>Follow some popular accounts</h2>
<button onClick={() => navigate("/")}>Skip</button>
{sortedRecommends.length > 0 && <FollowListBase pubkeys={sortedRecommends} />}
<button onClick={() => navigate("/")}>Done!</button>
</>
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress"></div>
</div>
<h1>
<FormattedMessage {...messages.Ready} />
</h1>
<p>
<FormattedMessage {...messages.Share} values={{ link: <Link to="/">{formatMessage(messages.World)}</Link> }} />
</p>
<h3>
<FormattedMessage {...messages.PopularAccounts} />
</h3>
{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} />}
</div>
);
}

View File

@ -0,0 +1,109 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { services } from "Pages/Verification";
import Nip5Service from "Element/Nip5Service";
import ProfileImage from "Element/ProfileImage";
import type { RootState } from "State/Store";
import { useUserProfile } from "Feed/ProfileFeed";
import messages from "./messages";
export default function GetVerified() {
const navigate = useNavigate();
const { publicKey } = useSelector((s: RootState) => s.login);
const user = useUserProfile(publicKey);
const [isVerified, setIsVerified] = useState(false);
const name = user?.name || "nostrich";
const [nip05, setNip05] = useState(`${name}@snort.social`);
const onNext = async () => {
navigate("/new/import");
};
return (
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress progress-third"></div>
</div>
<h1>
<FormattedMessage {...messages.Identifier} />
</h1>
<h4>
<FormattedMessage {...messages.PreviewOnSnort} />
</h4>
<div className="profile-preview-nip">
{publicKey && <ProfileImage pubkey={publicKey} defaultNip={nip05} verifyNip={false} />}
</div>
<p>
<FormattedMessage {...messages.IdentifierHelp} />
</p>
<ul>
<li>
<FormattedMessage {...messages.PreventFakes} />
</li>
<li>
<FormattedMessage {...messages.EasierToFind} />
</li>
<li>
<FormattedMessage {...messages.Funding} />
</li>
</ul>
<p className="warning">
<FormattedMessage {...messages.NameSquatting} />
</p>
{!isVerified && (
<>
<h2>
<FormattedMessage {...messages.GetSnortId} />
</h2>
<p>
<FormattedMessage {...messages.GetSnortIdHelp} />
</p>
<div className="nip-container">
<Nip5Service
key="snort"
{...services[0]}
helpText={false}
onChange={setNip05}
onSuccess={() => setIsVerified(true)}
/>
</div>
</>
)}
{!isVerified && (
<>
<h2>
<FormattedMessage {...messages.GetPartnerId} />
</h2>
<p>
<FormattedMessage {...messages.GetPartnerIdHelp} />
</p>
<div className="nip-container">
<Nip5Service
key="nostrplebs"
{...services[1]}
helpText={false}
onChange={setNip05}
onSuccess={() => setIsVerified(true)}
/>
</div>
</>
)}
<div className="next-actions">
{!isVerified && (
<button type="button" className="transparent" onClick={onNext}>
<FormattedMessage {...messages.Skip} />
</button>
)}
{isVerified && (
<button type="button" onClick={onNext}>
<FormattedMessage {...messages.Next} />
</button>
)}
</div>
</div>
);
}

View File

@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { ApiHost } from "Const";
import AsyncButton from "Element/AsyncButton";
@ -8,11 +9,14 @@ import { RootState } from "State/Store";
import { bech32ToHex } from "Util";
import { useNavigate } from "react-router-dom";
import messages from "./messages";
const TwitterFollowsApi = `${ApiHost}/api/v1/twitter/follows-for-nostr`;
export default function ImportFollows() {
const navigate = useNavigate();
const currentFollows = useSelector((s: RootState) => s.login.follows);
const { formatMessage } = useIntl();
const [twitterUsername, setTwitterUsername] = useState<string>("");
const [follows, setFollows] = useState<string[]>([]);
const [error, setError] = useState<string>("");
@ -29,45 +33,76 @@ export default function ImportFollows() {
const data = await rsp.json();
if (rsp.ok) {
if (Array.isArray(data) && data.length === 0) {
setError(`No nostr users found for "${twitterUsername}"`);
setError(formatMessage(messages.NoUsersFound, { twitterUsername }));
} else {
setFollows(data);
}
} else if ("error" in data) {
setError(data.error);
} else {
setError("Failed to load follows, please try again later");
setError(formatMessage(messages.FailedToLoad));
}
} catch (e) {
console.warn(e);
setError("Failed to load follows, please try again later");
setError(formatMessage(messages.FailedToLoad));
}
}
return (
<>
<h2>Import Twitter Follows</h2>
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress progress-last"></div>
</div>
<h1>
<FormattedMessage {...messages.ImportTwitter} />
</h1>
<p>
Find your twitter follows on nostr (Data provided by{" "}
<a href="https://nostr.directory" target="_blank" rel="noreferrer">
nostr.directory
</a>
)
<FormattedMessage
{...messages.FindYourFollows}
values={{
provider: (
<a href="https://nostr.directory" target="_blank" rel="noreferrer">
nostr.directory
</a>
),
}}
/>
</p>
<h2>
<FormattedMessage {...messages.TwitterUsername} />
</h2>
<div className="flex">
<input
type="text"
placeholder="Twitter username.."
placeholder={formatMessage(messages.TwitterPlaceholder)}
className="f-grow mr10"
value={twitterUsername}
onChange={e => setTwitterUsername(e.target.value)}
/>
<AsyncButton onClick={loadFollows}>Check</AsyncButton>
<AsyncButton type="button" className="secondary tall" onClick={loadFollows}>
<FormattedMessage {...messages.Check} />
</AsyncButton>
</div>
{error.length > 0 && <b className="error">{error}</b>}
{sortedTwitterFollows.length > 0 && <FollowListBase pubkeys={sortedTwitterFollows} />}
{sortedTwitterFollows.length > 0 && (
<FollowListBase
title={
<h2>
<FormattedMessage {...messages.FollowsOnNostr} values={{ username: twitterUsername }} />
</h2>
}
pubkeys={sortedTwitterFollows}
/>
)}
<button onClick={() => navigate("/new/discover")}>Next</button>
</>
<div className="next-actions">
<button className="secondary" type="button" onClick={() => navigate("/new/discover")}>
<FormattedMessage {...messages.Skip} />
</button>
<button type="button" onClick={() => navigate("/new/discover")}>
<FormattedMessage {...messages.Next} />
</button>
</div>
</div>
);
}

View File

@ -1,13 +0,0 @@
import ProfileSettings from "Pages/settings/Profile";
import { useNavigate } from "react-router-dom";
export default function NewUserProfile() {
const navigate = useNavigate();
return (
<>
<h1>Setup your Profile</h1>
<ProfileSettings privateKey={false} banner={false} />
<button onClick={() => navigate("/new/import")}>Next</button>
</>
);
}

View File

@ -0,0 +1,102 @@
import { useSelector } from "react-redux";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { CollapsedSection } from "Element/Collapsed";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
import { hexToBech32 } from "Util";
import messages from "./messages";
const WhatIsSnort = () => {
return (
<CollapsedSection title={<FormattedMessage {...messages.WhatIsSnort} />}>
<p>
<FormattedMessage {...messages.WhatIsSnortIntro} />
</p>
<p>
<FormattedMessage {...messages.WhatIsSnortNotes} />
</p>
<p>
<FormattedMessage {...messages.WhatIsSnortExperience} />
</p>
</CollapsedSection>
);
};
const HowDoKeysWork = () => {
return (
<CollapsedSection title={<FormattedMessage {...messages.HowKeysWork} />}>
<p>
<FormattedMessage {...messages.DigitalSignatures} />
</p>
<p>
<FormattedMessage {...messages.TamperProof} />
</p>
<p>
<FormattedMessage {...messages.Bitcoin} />
</p>
</CollapsedSection>
);
};
const Extensions = () => {
return (
<CollapsedSection title={<FormattedMessage {...messages.ImproveSecurity} />}>
<p>
<FormattedMessage {...messages.Extensions} />
</p>
<ul>
<li>
<a href="https://getalby.com/" target="_blank" rel="noreferrer">
Alby
</a>
</li>
<li>
<a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noreferrer">
nos2x
</a>
</li>
</ul>
<p>
<FormattedMessage {...messages.ExtensionsNostr} />
</p>
</CollapsedSection>
);
};
export default function NewUserFlow() {
const { publicKey, privateKey } = useSelector((s: RootState) => s.login);
const navigate = useNavigate();
return (
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress progress-first"></div>
</div>
<h1>
<FormattedMessage {...messages.SaveKeys} />
</h1>
<p>
<FormattedMessage {...messages.SaveKeysHelp} />
</p>
<h2>
<FormattedMessage {...messages.YourPubkey} />
</h2>
<Copy text={hexToBech32("npub", publicKey ?? "")} />
<h2>
<FormattedMessage {...messages.YourPrivkey} />
</h2>
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
<div className="next-actions">
<button type="button" onClick={() => navigate("/new/username")}>
<FormattedMessage {...messages.KeysSaved} />{" "}
</button>
</div>
<WhatIsSnort />
<HowDoKeysWork />
<Extensions />
</div>
);
}

View File

@ -0,0 +1,55 @@
import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import useEventPublisher from "Feed/EventPublisher";
import messages from "./messages";
export default function NewUserName() {
const [username, setUsername] = useState("");
const { formatMessage } = useIntl();
const publisher = useEventPublisher();
const navigate = useNavigate();
const onNext = async () => {
if (username.length > 0) {
const ev = await publisher.metadata({ name: username });
console.debug(ev);
publisher.broadcast(ev);
}
navigate("/new/verify");
};
return (
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress progress-second"></div>
</div>
<h1>
<FormattedMessage {...messages.PickUsername} />
</h1>
<p>
<FormattedMessage {...messages.UsernameHelp} />
</p>
<h2>
<FormattedMessage {...messages.Username} />
</h2>
<input
className="username"
placeholder={formatMessage(messages.UsernamePlaceholder)}
type="text"
value={username}
onChange={ev => setUsername(ev.target.value)}
/>
<div className="next-actions">
<button type="button" className="transparent" onClick={() => navigate("/new/verify")}>
<FormattedMessage {...messages.Skip} />
</button>
<button type="button" onClick={onNext}>
<FormattedMessage {...messages.Next} />
</button>
</div>
</div>
);
}

151
src/Pages/new/index.css Normal file
View File

@ -0,0 +1,151 @@
.new-user p {
color: var(--font-secondary-color);
}
.new-user li {
color: var(--font-secondary-color);
}
.new-user p > a {
color: var(--highlight);
}
.new-user li > a {
color: var(--highlight);
}
.new-user h1 {
color: var(--font-color);
font-weight: 700;
font-size: 32px;
line-height: 39px;
}
.new-user h2 {
color: var(--font-color);
font-weight: 600;
font-size: 16px;
line-height: 19px;
}
.new-user h3 {
color: var(--font-color);
font-weight: 700;
font-size: 21px;
line-height: 25px;
}
.new-user h4 {
color: var(--font-secondary-color);
font-weight: 600;
font-size: 12px;
line-height: 19px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.progress-bar {
width: 100%;
height: 7px;
background: var(--gray-secondary);
border-radius: 53px;
}
.progress-bar .progress {
height: 7px;
background: var(--snort-gradient);
border-radius: 53px;
}
.progress.progress-first {
width: 20%;
}
.progress.progress-second {
width: 50%;
}
.progress.progress-third {
width: 75%;
}
.progress.progress-last {
width: 95%;
}
.new-user .next-actions {
margin-top: 32px;
margin-bottom: 64px;
width: 100%;
display: flex;
justify-content: flex-end;
}
.new-user .next-actions button:not(:last-child) {
margin-right: 12px;
}
.new-user > .copy {
padding: 12px 16px;
border: 2px dashed #222222;
border-radius: 16px;
}
.light .new-user > .copy {
border: 2px dashed #aaaaaa;
}
.new-user > .copy .body {
font-size: 16px;
}
@media (max-width: 520px) {
.new-user > .copy .body {
font-size: 12px;
}
}
.new-user > .copy .icon {
margin-left: auto;
}
.new-user > .copy .icon svg {
width: 16px;
height: 16px;
}
.new-user input {
width: 100%;
max-width: 568px;
background: #222;
border: none;
}
@media (max-width: 720px) {
.new-user input {
width: calc(100vw - 40px);
}
}
.light .new-user input {
background: none;
}
.new-user .warning {
font-weight: 400;
font-size: 14px;
line-height: 19px;
color: #fc6e1e;
}
.profile-preview-nip {
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
.light .profile-preview-nip {
border: 1px solid rgba(0, 0, 0, 0.1);
}
.new-user .nip-container input[type="text"] {
width: 166px;
}

View File

@ -1,81 +1,36 @@
import { useSelector } from "react-redux";
import { RouteObject, useNavigate } from "react-router-dom";
import "./index.css";
import { RouteObject } from "react-router-dom";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
import { hexToBech32 } from "Util";
import NewUserProfile from "Pages/new//NewProfile";
import GetVerified from "Pages/new/GetVerified";
import NewUserName from "Pages/new/NewUsername";
import NewUserFlow from "Pages/new/NewUserFlow";
import ImportFollows from "Pages/new/ImportFollows";
import DiscoverFollows from "Pages/new/DiscoverFollows";
const USERNAME = "/new/username";
const IMPORT = "/new/import";
const DISCOVER = "/new/discover";
const VERIFY = "/new/verify";
export const NewUserRoutes: RouteObject[] = [
{
path: "/new",
element: <NewUserFlow />,
},
{
path: "/new/profile",
element: <NewUserProfile />,
path: USERNAME,
element: <NewUserName />,
},
{
path: "/new/import",
path: IMPORT,
element: <ImportFollows />,
},
{
path: "/new/discover",
path: VERIFY,
element: <GetVerified />,
},
{
path: DISCOVER,
element: <DiscoverFollows />,
},
];
export default function NewUserFlow() {
const { privateKey } = useSelector((s: RootState) => s.login);
const navigate = useNavigate();
return (
<>
<h1>Welcome to Snort!</h1>
<p>Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".</p>
<p>Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.</p>
<p>Snort is designed to have a similar experience to Twitter.</p>
<h2>Keys</h2>
<p>
Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many
relays to provide redundant storage of your content.
</p>
<p>
This means that nobody can modify notes which you have created and everybody can easily verify that the notes
they are reading are created by you.
</p>
<p>This is the same technology which is used by Bitcoin and has been proven to be extremely secure.</p>
<h2>Your Key</h2>
<p>
When you want to author new notes you need to sign them with your private key, as with Bitcoin private keys
these need to be kept secure.
</p>
<p>Please now copy your private key and save it somewhere secure:</p>
<div className="card">
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
</div>
<p>
It is also recommended to use one of the following browser extensions if you are on a desktop computer to secure
your key:
</p>
<ul>
<li>
<a href="https://getalby.com/" target="_blank" rel="noreferrer">
Alby
</a>
</li>
<li>
<a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noreferrer">
nos2x
</a>
</li>
</ul>
<p>You can also use these extensions to login to most Nostr sites.</p>
<button onClick={() => navigate("/new/profile")}>Next</button>
</>
);
}

57
src/Pages/new/messages.js Normal file
View File

@ -0,0 +1,57 @@
import { defineMessages } from "react-intl";
export default defineMessages({
SaveKeys: "Save your keys!",
SaveKeysHelp:
"Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.",
YourPubkey: "Your public key",
YourPrivkey: "Your private key",
KeysSaved: "I have saved my keys, continue",
WhatIsSnort: "What is Snort and how does it work?",
WhatIsSnortIntro: `Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".`,
WhatIsSnortNotes: `Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.`,
WhatIsSnortExperience: "Snort is designed to have a similar experience to Twitter.",
HowKeysWork: "How do keys work?",
DigitalSignatures: `Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.`,
TamperProof: `This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.`,
Bitcoin: `This is the same technology which is used by Bitcoin and has been proven to be extremely secure.`,
Extensions: `It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:`,
ExtensionsNostr: `You can also use these extensions to login to most Nostr sites.`,
ImproveSecurity: "Improve login security with browser extensions",
PickUsername: "Pick a username",
UsernameHelp:
"On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
Username: "Username",
UsernamePlaceholder: "e.g. Jack",
PopularAccounts: "Follow some popular accounts",
Skip: "Skip",
Done: "Done!",
ImportTwitter: "Import Twitter Follows (optional)",
TwitterPlaceholder: "Twitter username...",
FindYourFollows: "Find your twitter follows on nostr (Data provided by {provider})",
TwitterUsername: "Twitter username",
FollowsOnNostr: "{username}'s Follows on Nostr",
NoUsersFound: "No nostr users found for {twitterUsername}",
FailedToLoad: "Failed to load follows, please try again later",
Check: "Check",
Next: "Next",
SetupProfile: "Setup your Profile",
Identifier: "Get an identifier (optional)",
IdentifierHelp:
"Getting an identifier helps confirm the real you to people who know you. Many people can have a username @jack, but there is only one jack@cash.app.",
PreventFakes: "Prevent fake accounts from imitating you",
EasierToFind: "Make your profile easier to find and share",
Funding: "Fund developers and platforms providing NIP-05 verification services",
NameSquatting:
"Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
PreviewOnSnort: "Preview on snort",
GetSnortId: "Get a Snort identifier",
GetSnortIdHelp:
"Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.",
GetPartnerId: "Get a partner identifier",
GetPartnerIdHelp: "We have also partnered with nostrplebs.com to give you more options",
Ready: "You're ready!",
Share: "Share your thoughts with {link}",
World: "the world",
});