Keep the key backup in the health check list, raised through forceHealthChecks, and let a plan with no changes apply its own fix
This commit is contained in:
parent
c463e0e2db
commit
cb425734a3
7 changed files with 69 additions and 62 deletions
|
|
@ -47,8 +47,9 @@ only gate.
|
|||
component that needs app behavior takes it as a prop, or moves to `src/app/components`, as
|
||||
`IconInput` and its picker buttons did.
|
||||
- **app → components.** `routes.ts` (`goToChat` opens `ChatEnable`, `goToEvent` opens
|
||||
`NoteDetail`), `share.ts` (`Share`, `ShareEvent`), `deepLinks.ts` (`Search`) and `speech.ts`
|
||||
(`OpenRouterEnable`) import a component so they can open a modal mid-flow. Push adapters and
|
||||
`NoteDetail`), `share.ts` (`Share`, `ShareEvent`), `deepLinks.ts` (`Search`), `speech.ts`
|
||||
(`OpenRouterEnable`) and `healthChecks.ts` (`KeyDownload`, the fix for the key backup check)
|
||||
import a component so they can open a modal mid-flow. Push adapters and
|
||||
deep links reach those flows from outside any component, and the rest have several component
|
||||
callers, so each stays in one module. `editor/` holds `.svelte` files of its own (suggestion
|
||||
popovers), which `makeEditor` mounts.
|
||||
|
|
@ -162,7 +163,7 @@ size from `localStorage` before first paint. Then, in order:
|
|||
1. `restoreSession()` restores the saved session, if there is one, which builds a user-scoped
|
||||
`App` through `login`.
|
||||
2. It awaits `.ready` on the `synced` stores boot reads (device, wallet, push and notification
|
||||
settings, `shouldUnwrap`, `needsKeyBackup`).
|
||||
settings, `shouldUnwrap`, `forceHealthChecks`).
|
||||
3. It waits for storage, then `setupDeepLinks()` listens for warm-start links and handles a
|
||||
cold-start one.
|
||||
4. Each long-running subscription goes onto one `unsubscribers` list: `setupHistory`,
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ A derivation that every row subscribes to, or that joins large sets, is built by
|
|||
| repository, tracker, relays, relay stats, handles, zappers, plaintext, wraps | IndexedDB | yes | deleted |
|
||||
| settings (`SettingsValues`) | an encrypted app-data event, cached in IndexedDB | yes | local copy deleted |
|
||||
| `session`, `wallet` | `ss` | no | cleared |
|
||||
| `theme`, `flTheme`, `checked`, `shouldUnwrap`, `device`, `notificationSettings`, push state, `needsKeyBackup` | `kv` | no | cleared |
|
||||
| `theme`, `flTheme`, `checked`, `shouldUnwrap`, `device`, `notificationSettings`, push state, `forceHealthChecks` | `kv` | no | cleared |
|
||||
| drafts, dictations | a module `Map`, lost on reload | no | page reloads |
|
||||
|
||||
### IndexedDB (`src/app/storage.ts`, `src/lib/indexeddb.ts`)
|
||||
|
|
@ -288,13 +288,13 @@ or `withGetter(synced(...))` when callers need `.get()`, which keeps `.ready`. T
|
|||
`.ready` resolves once it has arrived:
|
||||
|
||||
- `theme`, `flTheme` (`theme.ts`), `checked` (`notifications.ts`), `shouldUnwrap` (`sync.ts`),
|
||||
`device` (`device.ts`), `needsKeyBackup` (`healthChecks.ts`) in `kv`
|
||||
`device` (`device.ts`), `forceHealthChecks` (`healthChecks.ts`) in `kv`
|
||||
- `pushState` (key `notificationState`) and `notificationSettings` in `kv`, both in
|
||||
`push/adapters/common.ts` and re-exported from `@app/push`
|
||||
- `wallet` (`lightning.ts`) in `ss`
|
||||
|
||||
The root layout awaits `.ready` for the stores boot code reads (`device`, `shouldUnwrap`,
|
||||
`wallet`, `notificationSettings`, `pushState`, `needsKeyBackup`) before `Push.sync()` and
|
||||
`wallet`, `notificationSettings`, `pushState`, `forceHealthChecks`) before `Push.sync()` and
|
||||
background sync start. A new store that boot reads joins that list. Never change a key, since
|
||||
it is where existing installs keep their value.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@
|
|||
const back = () => history.back()
|
||||
|
||||
const save = async (plan: HealthCheckPlan) => {
|
||||
const error = await publish(await plan.apply()).waitForError()
|
||||
const command = await plan.apply()
|
||||
const error = command && (await publish(command).waitForError())
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: `Your relays couldn't be saved: ${errorMessage(error)}`})
|
||||
|
|
|
|||
|
|
@ -7,52 +7,38 @@
|
|||
import HomeSection from "@app/components/HomeSection.svelte"
|
||||
import HealthCheckItem from "@app/components/HealthCheckItem.svelte"
|
||||
import HealthCheckReview from "@app/components/HealthCheckReview.svelte"
|
||||
import KeyDownload from "@app/components/KeyDownload.svelte"
|
||||
import type {HealthCheck} from "@app/healthChecks"
|
||||
import {healthChecks, needsKeyBackup} from "@app/healthChecks"
|
||||
import {session} from "@app/core"
|
||||
import {healthChecks} from "@app/healthChecks"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const pending = $healthChecks.pending.$
|
||||
|
||||
const count = $derived($pending.length + ($needsKeyBackup ? 1 : 0))
|
||||
// Changes are confirmed first, and fixes that change nothing to review run once they're saved.
|
||||
const review = (checks: HealthCheck[]) => {
|
||||
const plans = checks.map(check => $healthChecks.plan(check))
|
||||
const reviewed = plans.filter(plan => plan.changes.length > 0)
|
||||
const applyRest = () =>
|
||||
plans.filter(plan => plan.changes.length === 0).forEach(plan => plan.apply())
|
||||
|
||||
const finishKeyBackup = () => {
|
||||
needsKeyBackup.set(false)
|
||||
history.back()
|
||||
}
|
||||
|
||||
const backUpKey = () =>
|
||||
pushModal(KeyDownload, {
|
||||
secret: ($session!.data as {secret: string}).secret,
|
||||
next: finishKeyBackup,
|
||||
submitText: "Done",
|
||||
})
|
||||
|
||||
const review = (checks: HealthCheck[], next?: () => void) => {
|
||||
const plans = checks
|
||||
.map(check => $healthChecks.plan(check))
|
||||
.filter(plan => plan.changes.length > 0)
|
||||
|
||||
if (plans.length > 0) {
|
||||
pushModal(HealthCheckReview, {plans, next})
|
||||
if (reviewed.length > 0) {
|
||||
pushModal(HealthCheckReview, {plans: reviewed, next: applyRest})
|
||||
} else {
|
||||
next?.()
|
||||
applyRest()
|
||||
}
|
||||
}
|
||||
|
||||
const fix = (healthCheck: HealthCheck) => () => review([healthCheck])
|
||||
|
||||
const reviewAll = () => review($pending, $needsKeyBackup ? backUpKey : undefined)
|
||||
const reviewAll = () => review($pending)
|
||||
</script>
|
||||
|
||||
<HomeSection title="Health checks" icon={Stethoscope}>
|
||||
{#snippet action()}
|
||||
{#if count > 0}
|
||||
<span class="badge badge-warning">{count} open</span>
|
||||
{#if $pending.length > 0}
|
||||
<span class="badge badge-warning">{$pending.length} open</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#if count === 0}
|
||||
{#if $pending.length === 0}
|
||||
<p class="flex items-center gap-2 px-4 pb-4 text-sm opacity-75">
|
||||
<Icon icon={CheckCircle} size={4} />
|
||||
Your connection to the network looks healthy.
|
||||
|
|
@ -65,13 +51,7 @@
|
|||
description={healthCheck.description}
|
||||
onfix={fix(healthCheck)} />
|
||||
{/each}
|
||||
{#if $needsKeyBackup}
|
||||
<HealthCheckItem
|
||||
title="Back Up Your Key"
|
||||
description="Save a backup of your private key in a secure place."
|
||||
onfix={backUpKey} />
|
||||
{/if}
|
||||
{#if count > 1}
|
||||
{#if $pending.length > 1}
|
||||
<div class="flex justify-center p-3">
|
||||
<Button class="button button-primary button-sm" onclick={reviewAll}>
|
||||
<Icon icon={Stars} size={4} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {assoc} from "@welshman/lib"
|
||||
import type {ClientOptions} from "@pomade/core"
|
||||
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
||||
import {Profile} from "@welshman/domain"
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
DEFAULT_MESSAGING_RELAYS,
|
||||
DEFAULT_SPACES,
|
||||
} from "@app/env"
|
||||
import {needsKeyBackup} from "@app/healthChecks"
|
||||
import {forceHealthChecks} from "@app/healthChecks"
|
||||
import {loginWithPomade} from "@app/pomade"
|
||||
import {pushModal, clearModals} from "@app/modal"
|
||||
import {app, domain, login, roomLists} from "@app/core"
|
||||
|
|
@ -92,7 +93,7 @@
|
|||
const secret = getKey<string>("signup.secret")!
|
||||
|
||||
await login(toSession(nip01, {secret}))
|
||||
needsKeyBackup.set(true)
|
||||
forceHealthChecks.update(assoc("backup-key", true))
|
||||
await completeSignup()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import {derived} from "svelte/store"
|
||||
import {sample} from "@welshman/lib"
|
||||
import {assoc, sample} from "@welshman/lib"
|
||||
import {normalizeRelayUrl} from "@welshman/util"
|
||||
import {synced} from "@welshman/store"
|
||||
import {MessagingRelayLists, RelayLists, Relays, SearchRelayLists, projection} from "@welshman/app"
|
||||
import type {Command, IApp, Projection} from "@welshman/app"
|
||||
import {usePlugin} from "@app/core"
|
||||
import {session, usePlugin} from "@app/core"
|
||||
import {DEFAULT_RELAYS} from "@app/env"
|
||||
import {kv} from "@app/storage"
|
||||
import {pushModal} from "@app/modal"
|
||||
import KeyDownload from "@app/components/KeyDownload.svelte"
|
||||
|
||||
export type HealthCheckContext = {
|
||||
readRelays: string[]
|
||||
|
|
@ -21,10 +23,11 @@ export type HealthCheckChange = {
|
|||
removed: string[]
|
||||
}
|
||||
|
||||
// A plan with changes is reviewed and returns the Command to publish; one without does the fix itself.
|
||||
export type HealthCheckPlan = {
|
||||
summary: string
|
||||
changes: HealthCheckChange[]
|
||||
apply: () => Promise<Command>
|
||||
apply: () => Promise<Command | void>
|
||||
}
|
||||
|
||||
export type HealthCheck = {
|
||||
|
|
@ -94,8 +97,12 @@ const tooManyRelaysCheck = (spec: RelayListSpec): HealthCheck => ({
|
|||
),
|
||||
})
|
||||
|
||||
// Set when a key is generated in-app, and cleared once the user has saved a backup of it
|
||||
export const needsKeyBackup = synced({key: "needsKeyBackup", storage: kv, defaultValue: false})
|
||||
// Raises or clears a check by id regardless of what its own isPending says.
|
||||
export const forceHealthChecks = synced<Record<string, boolean>>({
|
||||
key: "forceHealthChecks",
|
||||
storage: kv,
|
||||
defaultValue: {},
|
||||
})
|
||||
|
||||
export class HealthChecks {
|
||||
context: Projection<HealthCheckContext>
|
||||
|
|
@ -125,8 +132,10 @@ export class HealthChecks {
|
|||
)
|
||||
|
||||
this.pending = projection(
|
||||
derived([this.context.$, app.use(Relays).index.$], ([$context]) =>
|
||||
this.checks.filter(check => check.isPending($context)),
|
||||
derived(
|
||||
[this.context.$, forceHealthChecks, app.use(Relays).index.$],
|
||||
([$context, $forceHealthChecks]) =>
|
||||
this.checks.filter(check => $forceHealthChecks[check.id] ?? check.isPending($context)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -184,8 +193,31 @@ export class HealthChecks {
|
|||
urls => this.app.use(SearchRelayLists).setUrls(urls),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "backup-key",
|
||||
title: "Back Up Your Key",
|
||||
description: "Save a backup of your private key in a secure place.",
|
||||
// Only a key generated in-app is ours to back up, so signup raises this through forceHealthChecks.
|
||||
isPending: () => false,
|
||||
plan: () => ({
|
||||
summary: "Opens the key backup dialog, where you choose how to save your key.",
|
||||
changes: [],
|
||||
apply: async () => {
|
||||
pushModal(KeyDownload, {
|
||||
secret: (session.get()!.data as {secret: string}).secret,
|
||||
next: this.finishKeyBackup,
|
||||
submitText: "Done",
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
private finishKeyBackup = () => {
|
||||
forceHealthChecks.update(assoc("backup-key", false))
|
||||
history.back()
|
||||
}
|
||||
|
||||
plan = (healthCheck: HealthCheck) => healthCheck.plan(this.context.get())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@
|
|||
import {restoreSession} from "@app/session"
|
||||
import {syncSignerAlerts} from "@app/signer"
|
||||
import {wallet} from "@app/lightning"
|
||||
import {kv, storage} from "@app/storage"
|
||||
import {storage} from "@app/storage"
|
||||
import {device} from "@app/device"
|
||||
import {needsKeyBackup} from "@app/healthChecks"
|
||||
import {forceHealthChecks} from "@app/healthChecks"
|
||||
import {setupShareIntents} from "@app/share"
|
||||
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
||||
import * as env from "@app/env"
|
||||
|
|
@ -89,17 +89,9 @@
|
|||
wallet.ready,
|
||||
notificationSettings.ready,
|
||||
pushState.ready,
|
||||
needsKeyBackup.ready,
|
||||
forceHealthChecks.ready,
|
||||
])
|
||||
|
||||
// The key backup reminder used to live in a forceHealthChecks record
|
||||
const legacyHealthChecks = await kv.get<Record<string, boolean>>("forceHealthChecks")
|
||||
|
||||
if (legacyHealthChecks) {
|
||||
needsKeyBackup.set(Boolean(legacyHealthChecks["backup-key"]))
|
||||
await kv.remove("forceHealthChecks")
|
||||
}
|
||||
|
||||
// Wait for critical storage data only
|
||||
await storage.get()?.ready
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue