Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/set-username-in-init.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"playground-cli": minor
---

`dot init` now prompts you to claim a playground.dot username when one
isn't already set on the registry. If you accept, the CLI signs a
`setUsername` tx against the registry contract and surfaces the chosen
name in the top breadcrumb alongside the command, network, and version.
Runs that find an existing username read it from the registry (best-block
freshness, same as the playground-app) and skip the prompt — your handle
just shows in the header.

Declining is non-destructive: pick "No" and `dot init` continues as
before. The choice is not persisted, so re-running `dot init` will prompt
again until a name is claimed.
90 changes: 17 additions & 73 deletions src/commands/init/IdentityLines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,106 +13,50 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { useEffect, useState } from "react";
import { Row, Section } from "../../utils/ui/theme/index.js";
import type { SessionAddresses } from "../../utils/auth.js";
import {
formatUsernameLine,
lookupUsername,
lookupRegistryUsername,
type UsernameLookup,
} from "../../utils/username.js";
import { PLAYGROUND_PRODUCT_ID } from "../../config.js";

/**
* Four-row identity block shown after a successful login:
* Two-row identity block shown after a successful login:
*
* logged in <wallet root SS58>
* username <name> (playground|polkadot)
* account in use playground.dot/0 — <product 0x H160>
* product account <product SS58>
* account in use playground.dot/0 — <product 0x H160> · <product SS58>
*
* `logged in` is the SSO-handshake `rootAccountId` (bare-mnemonic on
* current mobile builds). It is the storage key for the People-parachain
* username lookup. It is NOT the same address mobile shows as "Wallet
* account" on its debug screen — that uses the hard `//wallet`
* current mobile builds). It is NOT the same address mobile shows as
* "Wallet account" on its debug screen — that uses the hard `//wallet`
* derivation which the host can't reproduce.
*
* `username` follows a two-tier precedence, same first tier as the
* playground-app's `displayNameForAccount`:
* 1. registry username — the handle the user set in the playground-app
* profile (`registry.setUsername`), keyed on the product H160 since
* that's the `caller()` the contract records.
* 2. People-parachain identity from `lookupUsername` — the chain-wide
* handle, keyed on the root SS58.
* The source ("playground" vs "polkadot") is suffixed so it's obvious
* which surface the user is seeing. If neither resolves, the row falls
* through to `formatUsernameLine`'s `(no username set on chain)`.
* `account in use` is a single row that surfaces all three views of
* the playground product account: the derivation slug, the H160 (what
* Revive sees as `caller()` and what playground-app displays), and the
* SS58 (what subscan / wallet tooling shows). Two earlier rows
* (`account in use` + `product account`) showed the H160 and SS58
* separately — same key, two encodings — which read as "two accounts"
* to users. Collapsed here.
*
* `account in use` surfaces the derivation slug + the H160 that signs
* on the user's behalf, so the user can verify the exact account
* without inspecting the SS58. `product account` keeps the SS58 form
* on its own row.
*
* Both lookups are async, fired in parallel, each cancellable. The
* People-parachain query has a 10s timeout inside `lookupUsername`;
* the registry query degrades silently to `null` on any error
* (`lookupRegistryUsername`) so older deploys without `getUsername`
* fall through to tier 2 without surfacing an error to the user.
* The user's registry username is intentionally NOT rendered here — it
* lives in the top breadcrumb (see `Header`'s `username` prop) so it
* stays visible across every screen in the command. `UsernamePrompt`
* owns the read + write path; this component is purely the address
* pair now.
*
* The SS58 + H160 are taken straight off the auth-derived pair so
* they never drift — the bug we had previously was running
* `deriveProductAccountPublicKey` again on the already-derived SS58
* and producing a doubly-derived ghost address.
*/
export function IdentityLines({ addresses }: { addresses: SessionAddresses }) {
const [walletUsername, setWalletUsername] = useState<UsernameLookup>({ kind: "loading" });
// null means "lookup completed, no registry username set"; undefined means
// "still loading". Display rule: prefer registry > People > fall back to
// the H160 — same precedence the playground-app uses in `displayNameForAccount`.
const [registryUsername, setRegistryUsername] = useState<string | null | undefined>(undefined);

useEffect(() => {
let cancelled = false;
lookupUsername(addresses.rootAddress).then((result) => {
if (!cancelled) setWalletUsername(result);
});
lookupRegistryUsername(addresses.productH160 as `0x${string}`).then((result) => {
if (!cancelled) setRegistryUsername(result);
});
return () => {
cancelled = true;
};
}, [addresses.rootAddress, addresses.productH160]);

const usernameLine = registryUsername
? registryUsername
: registryUsername === undefined
? "(looking up...)"
: formatUsernameLine(walletUsername);
const usernameTone = registryUsername || walletUsername.kind === "found" ? "default" : "muted";
const usernameSource = registryUsername
? "playground"
: walletUsername.kind === "found"
? "polkadot"
: null;

return (
<Section>
<Row mark="ok" label="logged in" value={addresses.rootAddress} tone="muted" />
<Row
mark="ok"
label="username"
value={usernameSource ? `${usernameLine} (${usernameSource})` : usernameLine}
tone={usernameTone}
/>
<Row
mark="ok"
label="account in use"
value={`${PLAYGROUND_PRODUCT_ID}/0 — ${addresses.productH160}`}
value={`${PLAYGROUND_PRODUCT_ID}/0 — ${addresses.productH160} · ${addresses.productAddress}`}
tone="muted"
/>
<Row mark="ok" label="product account" value={addresses.productAddress} tone="muted" />
</Section>
);
}
22 changes: 21 additions & 1 deletion src/commands/init/InitScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { DependencyList } from "./DependencyList.js";
import { IdentityLines } from "./IdentityLines.js";
import { QrLogin } from "./QrLogin.js";
import { AccountSetup } from "./AccountSetup.js";
import { UsernamePrompt } from "./UsernamePrompt.js";
import { computeAllDone } from "./completion.js";
import { VERSION_LABEL } from "../../utils/version.js";
import { getNetworkLabel } from "../../config.js";
Expand All @@ -40,13 +41,18 @@ export function InitScreen({
const [depsComplete, setDepsComplete] = useState(false);
const [accountComplete, setAccountComplete] = useState(false);
const [accountOk, setAccountOk] = useState(true);
// `null` ≡ "no username on chain and user declined to set one";
// `string` ≡ "username known (existing or just-claimed)".
// `undefined` ≡ "prompt has not resolved yet".
const [username, setUsername] = useState<string | null | undefined>(undefined);

const allDone = computeAllDone({
needsQr,
authResolved,
loggedInAddress: addresses?.productAddress ?? null,
depsComplete,
accountComplete,
usernameComplete: username !== undefined,
});

const handleDepsDone = () => {
Expand All @@ -61,6 +67,16 @@ export function InitScreen({
const handleAccountDone = (success: boolean) => {
setAccountOk(success);
setAccountComplete(true);
// Account setup is a prerequisite for setUsername (the tx needs the
// smart-contract allowance + a funded product account). When account
// setup fails we skip the prompt entirely and treat the step as
// resolved-with-no-username so the init flow can land on
// "setup complete (with errors)" instead of hanging.
if (!success) setUsername(null);
};

const handleUsernameDone = (next: string | null) => {
setUsername(next);
};

useEffect(() => {
Expand All @@ -71,8 +87,8 @@ export function InitScreen({
<Box flexDirection="column">
<Header
cmd="dot init"
subtitle="polkadot playground"
network={getNetworkLabel()}
username={username ?? undefined}
right={VERSION_LABEL}
/>

Expand All @@ -86,6 +102,10 @@ export function InitScreen({
<AccountSetup address={addresses.productAddress} onDone={handleAccountDone} />
)}

{addresses && accountComplete && accountOk && (
<UsernamePrompt addresses={addresses} onDone={handleUsernameDone} />
)}

{allDone && (
<Section gapBelow={false}>
<Row
Expand Down
Loading
Loading