@vibes.diy/prompts 14.3.174 → 14.3.176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/llms/backend.md +5 -1
- package/llms/spotify.md +149 -72
- package/llms/stripe-subscriptions.md +196 -117
- package/llms/stripe.md +130 -93
- package/llms/use-vibe.md +77 -0
- package/llms/youtube.md +147 -66
- package/package.json +6 -6
package/llms/stripe.md
CHANGED
|
@@ -36,11 +36,15 @@ merchant. An app where *each visitor* collects money into *their own* Stripe
|
|
|
36
36
|
account is a platform (Stripe Connect) and is a different thing this recipe
|
|
37
37
|
does not build — say so plainly and build the one-merchant half.
|
|
38
38
|
|
|
39
|
-
## 1.
|
|
39
|
+
## 1. The setup item: the owner's Stripe secret key
|
|
40
40
|
|
|
41
|
-
The owner gets a key from Stripe, once. The
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
The owner gets a key from Stripe, once. The Setup surface it lands on — the
|
|
42
|
+
owner-only entry in the app's navigation, the verdict from `/setup`, the
|
|
43
|
+
re-read on focus that unlocks the Pay buttons in place — is the use-vibe
|
|
44
|
+
docs' *Some apps need setup before they work*; this recipe contributes one
|
|
45
|
+
item to it. The Stripe item is the secret key: the backend answers missing-key while ctx.secrets.STRIPE_SECRET_KEY is absent, and its steps link Stripe's API keys page by URL and name STRIPE_SECRET_KEY in so many words.
|
|
46
|
+
The steps are written for someone who has never opened Stripe, so each one is
|
|
47
|
+
a link to the exact page and uses the words that page uses:
|
|
44
48
|
|
|
45
49
|
1. Open Stripe's **API keys** page in test mode:
|
|
46
50
|
<https://dashboard.stripe.com/test/apikeys> (sign in, or create a Stripe
|
|
@@ -54,34 +58,29 @@ the words that page uses:
|
|
|
54
58
|
3. Come back to the app. The Pay buttons work the moment the key is saved; no
|
|
55
59
|
redeploy. In test mode the card `4242 4242 4242 4242` with any future date
|
|
56
60
|
pays.
|
|
57
|
-
4.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
the
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
with real links: `https://dashboard.stripe.com/test/apikeys` for the key,
|
|
75
|
-
`https://vibes.diy/vibes/mine/<ownerHandle>/<appSlug>/settings` for where
|
|
76
|
-
it goes, and `STRIPE_SECRET_KEY` in so many words. "Open the Stripe
|
|
77
|
-
dashboard" and "the Settings page" are not instructions; a person who has
|
|
78
|
-
never seen either would have to search for both. A visitor who is not the
|
|
79
|
-
owner sees a calmer sentence — payments are not set up yet — never the steps.
|
|
61
|
+
4. Go live. When a test payment goes through, switch to your live key to take
|
|
62
|
+
real payments: open Stripe's **API keys** page in live mode,
|
|
63
|
+
<https://dashboard.stripe.com/apikeys>, press **Reveal live key** beside
|
|
64
|
+
**Secret key** (it starts with `sk_live_`), and set `STRIPE_SECRET_KEY` to
|
|
65
|
+
it the same way; setting an existing key replaces it. This step stays on
|
|
66
|
+
the Setup screen after the item is done, with a badge that reads *Test
|
|
67
|
+
mode* until the live key is in.
|
|
68
|
+
|
|
69
|
+
Those steps are the item's copy on the Setup surface, with the vibe's real
|
|
70
|
+
handle and slug filled in from the `ownerHandle` and `appSlug` the `/setup`
|
|
71
|
+
route hands over. `https://dashboard.stripe.com/test/apikeys` for the key,
|
|
72
|
+
`https://vibes.diy/vibes/mine/<ownerHandle>/<appSlug>/settings` for where it
|
|
73
|
+
goes, and `STRIPE_SECRET_KEY` in so many words: "Open the Stripe dashboard"
|
|
74
|
+
and "the Settings page" are directions to search for, and a person who has
|
|
75
|
+
never seen either has to go looking. In the consumer flow, a Pay button whose
|
|
76
|
+
key is not set reads *Checkout is not open yet*; the owner's Setup entry is
|
|
77
|
+
where the steps are.
|
|
80
78
|
|
|
81
79
|
Only the owner can set or rotate a secret, and handlers see the current value on
|
|
82
80
|
every invocation — so the pay button starts working the moment the key lands,
|
|
83
81
|
with no redeploy. There is no publishable key, no webhook signing secret and no
|
|
84
|
-
second secret of any kind in this recipe: one key, held by the server.
|
|
82
|
+
second secret of any kind in this recipe: one key, held by the server. Later,
|
|
83
|
+
the same Setup entry is where the owner swaps the test key for the live one.
|
|
85
84
|
|
|
86
85
|
## 2. The key never reaches the browser, and neither does Stripe.js
|
|
87
86
|
|
|
@@ -308,11 +307,16 @@ async function settle(ctx, order) {
|
|
|
308
307
|
export async function fetch(request, ctx) {
|
|
309
308
|
const url = new URL(request.url);
|
|
310
309
|
|
|
311
|
-
//
|
|
312
|
-
|
|
310
|
+
// The setup verdict: ready for everyone, the items (and the handle + slug the Setup screen
|
|
311
|
+
// links with) for the owner alone.
|
|
312
|
+
if (url.pathname === "/setup" && request.method === "GET") {
|
|
313
313
|
const owner = ctx.userInfo?.userHandle === ctx.appInfo.ownerHandle;
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
const items = [{ id: "stripe-key", label: "Stripe secret key", done: Boolean(ctx.secrets.STRIPE_SECRET_KEY) }];
|
|
315
|
+
const ready = items.every((item) => item.done);
|
|
316
|
+
if (!owner) return json({ ok: true, ready, owner });
|
|
317
|
+
// `live` is the Go-live step's badge: a test key takes test payments, a live key real ones.
|
|
318
|
+
const live = (ctx.secrets.STRIPE_SECRET_KEY ?? "").startsWith("sk_live_");
|
|
319
|
+
return json({ ok: true, ready, owner, live, items, ownerHandle: ctx.appInfo.ownerHandle, appSlug: ctx.appInfo.appSlug });
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
// Start a purchase: the order first, then a Checkout Session for exactly that order.
|
|
@@ -421,32 +425,68 @@ export function orders(doc, oldDoc, user, ctx) {
|
|
|
421
425
|
|
|
422
426
|
### `App.jsx`
|
|
423
427
|
```jsx app=stripe-dues
|
|
424
|
-
import React, { useEffect, useState } from "react";
|
|
428
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
425
429
|
import { useFireproof } from "use-fireproof";
|
|
426
430
|
import { apiFetch, useVibe } from "use-vibes";
|
|
427
|
-
import { Badge, Button, Heading, Stack, Text } from "@vibes.diy/pouf";
|
|
431
|
+
import { Badge, Button, Heading, Row, Stack, Text } from "@vibes.diy/pouf";
|
|
428
432
|
|
|
429
|
-
// The
|
|
430
|
-
|
|
431
|
-
|
|
433
|
+
// The setup verdict, read on mount and again whenever the app comes back into view: the owner
|
|
434
|
+
// sets the key in another tab and returns, and the Pay buttons unlock without a reload.
|
|
435
|
+
function useSetup() {
|
|
436
|
+
const [setup, setSetup] = useState(null); // { ready, owner, items?, ownerHandle?, appSlug? }
|
|
437
|
+
const latest = useRef(0);
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
// Mount, focus and visibility reads overlap; only the latest one may land, or a slow stale
|
|
440
|
+
// answer overwrites a fresh one.
|
|
441
|
+
const load = () => {
|
|
442
|
+
const n = ++latest.current;
|
|
443
|
+
apiFetch("/setup").then((r) => r.json()).catch(() => ({ ready: false, owner: false })).then((s) => { if (n === latest.current) setSetup(s); });
|
|
444
|
+
};
|
|
445
|
+
load();
|
|
446
|
+
const onVisible = () => { if (document.visibilityState === "visible") load(); };
|
|
447
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
448
|
+
window.addEventListener("focus", load);
|
|
449
|
+
return () => { document.removeEventListener("visibilitychange", onVisible); window.removeEventListener("focus", load); };
|
|
450
|
+
}, []);
|
|
451
|
+
return setup;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// The Setup surface, the owner's alone: one item, every step a link to the exact page in that
|
|
455
|
+
// page's own words, with this vibe's real handle and slug filled in.
|
|
456
|
+
function SetupScreen({ setup }) {
|
|
457
|
+
const vibe = `${setup.ownerHandle}/${setup.appSlug}`;
|
|
432
458
|
const settingsUrl = `https://vibes.diy/vibes/mine/${vibe}/settings`;
|
|
459
|
+
const key = setup.items.find((item) => item.id === "stripe-key");
|
|
433
460
|
return (
|
|
434
|
-
<
|
|
435
|
-
<
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
</
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
461
|
+
<Stack>
|
|
462
|
+
<Heading>Setup</Heading>
|
|
463
|
+
<Row gap={2}>
|
|
464
|
+
<Text>{key.label}</Text>
|
|
465
|
+
<Badge tone={key.done ? "up" : "info"}>{key.done ? "Done" : "To do"}</Badge>
|
|
466
|
+
</Row>
|
|
467
|
+
<ol>
|
|
468
|
+
<li>
|
|
469
|
+
Open Stripe's <a href="https://dashboard.stripe.com/test/apikeys" target="_blank" rel="noreferrer">API keys page (test mode)</a>,
|
|
470
|
+
use your Stripe account (the page offers to create one), and under <b>Standard keys</b> press <b>Reveal test key</b> beside <b>Secret key</b>.
|
|
471
|
+
Copy it; it starts with <code>sk_test_</code>.
|
|
472
|
+
</li>
|
|
473
|
+
<li>
|
|
474
|
+
Open <a href={settingsUrl} target="_blank" rel="noreferrer">this vibe's Settings tab</a> (My Vibes → this vibe → Settings).
|
|
475
|
+
Under <b>Secrets</b>, enter the key <code>STRIPE_SECRET_KEY</code>, paste the value, and save.
|
|
476
|
+
From a terminal instead: <code>npx vibes-diy secrets set STRIPE_SECRET_KEY --vibe {vibe}</code>.
|
|
477
|
+
</li>
|
|
478
|
+
<li>
|
|
479
|
+
Come back here: the Pay buttons work as soon as the key is saved. In test mode the card 4242 4242 4242 4242 pays.
|
|
480
|
+
</li>
|
|
481
|
+
<li>
|
|
482
|
+
{/* The Go-live step stays here once the item is done; its badge says which key is in. */}
|
|
483
|
+
<Badge tone={setup.live ? "up" : "info"}>{setup.live ? "Live" : "Test mode"}</Badge>{" "}
|
|
484
|
+
When a test payment goes through, switch to your live key to take real payments: open Stripe's{" "}
|
|
485
|
+
<a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noreferrer">API keys page (live mode)</a>, press <b>Reveal live key</b> beside <b>Secret key</b>
|
|
486
|
+
(it starts with <code>sk_live_</code>), and set <code>STRIPE_SECRET_KEY</code> to it the same way.
|
|
487
|
+
</li>
|
|
488
|
+
</ol>
|
|
489
|
+
</Stack>
|
|
450
490
|
);
|
|
451
491
|
}
|
|
452
492
|
|
|
@@ -454,23 +494,13 @@ export default function App() {
|
|
|
454
494
|
const { me } = useVibe("orders");
|
|
455
495
|
const { useLiveQuery } = useFireproof("orders");
|
|
456
496
|
const { docs: orders } = useLiveQuery("type", { key: "order" });
|
|
457
|
-
const
|
|
497
|
+
const setup = useSetup();
|
|
498
|
+
const [screen, setScreen] = useState("membership");
|
|
458
499
|
const [error, setError] = useState(null);
|
|
459
500
|
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
const load = () => apiFetch("/status").then((r) => r.json()).then(setStatus).catch(() => setStatus({ configured: false, owner: false }));
|
|
464
|
-
load();
|
|
465
|
-
const onVisible = () => { if (document.visibilityState === "visible") load(); };
|
|
466
|
-
document.addEventListener("visibilitychange", onVisible);
|
|
467
|
-
window.addEventListener("focus", load);
|
|
468
|
-
return () => { document.removeEventListener("visibilitychange", onVisible); window.removeEventListener("focus", load); };
|
|
469
|
-
}, []);
|
|
470
|
-
|
|
471
|
-
// Back from Stripe: ask the backend to read the session and settle the order.
|
|
472
|
-
// Back from Stripe. The wrapper hands the app its fragment just after first render and fires
|
|
473
|
-
// hashchange, so read the hash now AND on every change — a mount-only read can miss it.
|
|
501
|
+
// Back from Stripe: ask the backend to read the session and settle the order. The wrapper hands
|
|
502
|
+
// the app its fragment just after first render and fires hashchange, so read the hash now AND on
|
|
503
|
+
// every change — a mount-only read can miss it.
|
|
474
504
|
useEffect(() => {
|
|
475
505
|
const confirm = () => {
|
|
476
506
|
const m = /#paid=([\w-]+)/.exec(location.hash);
|
|
@@ -506,36 +536,43 @@ export default function App() {
|
|
|
506
536
|
}
|
|
507
537
|
};
|
|
508
538
|
|
|
509
|
-
const
|
|
510
|
-
if (status && !configured && status.owner) {
|
|
511
|
-
return (
|
|
512
|
-
<Stack>
|
|
513
|
-
<Heading>Set up payments</Heading>
|
|
514
|
-
<Text>Add your Stripe secret key once and the Pay buttons go live for everyone.</Text>
|
|
515
|
-
<SetupSteps status={status} />
|
|
516
|
-
</Stack>
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
539
|
+
const ready = setup?.ready === true;
|
|
520
540
|
const mine = orders.filter((o) => o.buyerHandle === me?.userHandle);
|
|
521
541
|
const paid = mine.some((o) => o.kind === "dues" && o.status === "paid");
|
|
522
542
|
return (
|
|
523
543
|
<Stack>
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
544
|
+
<Row gap={2}>
|
|
545
|
+
<Button variant="quiet" onClick={() => setScreen("membership")}>Membership</Button>
|
|
546
|
+
{/* The Setup entry: the owner's alone, highlighted while an item is still to do, quiet once done. */}
|
|
547
|
+
{setup?.owner && (
|
|
548
|
+
<Button variant={ready ? "quiet" : "solid"} onClick={() => setScreen("setup")}>
|
|
549
|
+
Setup{ready ? "" : " · to do"}
|
|
550
|
+
</Button>
|
|
551
|
+
)}
|
|
552
|
+
</Row>
|
|
553
|
+
{screen === "setup" && setup?.owner ? (
|
|
554
|
+
<SetupScreen setup={setup} />
|
|
527
555
|
) : (
|
|
528
|
-
<
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
556
|
+
<Stack>
|
|
557
|
+
<Heading>Membership</Heading>
|
|
558
|
+
{paid ? (
|
|
559
|
+
<Badge tone="up">Dues paid</Badge>
|
|
560
|
+
) : ready ? (
|
|
561
|
+
<Button onClick={() => pay("dues")} disabled={busy} loading={busy}>
|
|
562
|
+
Pay $40 dues — Stripe opens in a new tab
|
|
563
|
+
</Button>
|
|
564
|
+
) : (
|
|
565
|
+
<Text muted>Checkout is not open yet.</Text>
|
|
566
|
+
)}
|
|
567
|
+
{error && <Text>{error}</Text>}
|
|
568
|
+
{setup?.owner && (
|
|
569
|
+
<ul>
|
|
570
|
+
{orders.map((o) => (
|
|
571
|
+
<li key={o._id}>@{o.buyerHandle} · {o.title} · {(o.amount / 100).toFixed(2)} {o.currency.toUpperCase()} · {o.status}</li>
|
|
572
|
+
))}
|
|
573
|
+
</ul>
|
|
574
|
+
)}
|
|
575
|
+
</Stack>
|
|
539
576
|
)}
|
|
540
577
|
</Stack>
|
|
541
578
|
);
|
package/llms/use-vibe.md
CHANGED
|
@@ -81,6 +81,83 @@ function PromptBar({ database }) {
|
|
|
81
81
|
|
|
82
82
|
Don't gate management UI on a display flag directly. Encode the rule in `access.js` (e.g. `ctx.requireRole("owner")` — the owner is auto-seeded into the reserved `owner` role) and gate the UI on `can.*` for that database — the verdict reflects the same rule. Per-row edit/delete affordances: `{can.edit(doc).ok && <EditButton doc={doc} />}`. By default every signed-in visitor is a first-class participant who creates and edits their own data; reserve `requireRole("owner")` for genuinely owner-published apps and per-object channels for peer-to-peer sharing. Public-vs-private and the allowed-user list are the owner's runtime sharing settings, not `access.js`.
|
|
83
83
|
|
|
84
|
+
## Some apps need setup before they work
|
|
85
|
+
|
|
86
|
+
Most apps work the moment they open. Some need their owner to do one thing first — paste a key, save a billing portal, connect an account — and that is a property of the app, not of any one integration, so every app that has it takes the same shape. An app has at most one **Setup** surface, and the backend's verdict decides its state, never a display flag. The integration skills (Stripe, Spotify, YouTube) each contribute one item to it: what the app needs, how the backend checks it, and the linked steps in the provider's own words.
|
|
87
|
+
|
|
88
|
+
**The backend answers what the app needs and whether each item is done.** A `/setup` route in `backend.js`, called with `apiFetch` so `ctx.userInfo` is set, lists the items — the key is present, the portal is saved, the account is connected — with `done` read from `ctx.secrets` or the connection, and `ready` when every item is done. An item whose verdict costs a provider call (is the portal saved?) is asked on the owner's call alone and kept in a document the backend writes; a visitor's read takes the kept value, so visitor traffic spends nothing of the provider's and a blip there keeps the last verdict. The Setup surface is the owner's alone: the backend decides who the owner is, and non-owners never receive the items. Each step is a link to the exact page in that page's own words, with the vibe's real handle and slug filled in from what the backend hands the client. The route carries `ownerHandle` and `appSlug` from `ctx.appInfo`, so the Settings link (`https://vibes.diy/vibes/mine/<ownerHandle>/<appSlug>/settings`, **Secrets**) and the CLI line (`npx vibes-diy secrets set KEY --vibe <ownerHandle>/<appSlug>`) are copy-ready rather than templates.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// backend.js — the verdict for everyone, the items for the owner alone
|
|
92
|
+
if (url.pathname === "/setup" && request.method === "GET") {
|
|
93
|
+
const owner = ctx.userInfo?.userHandle === ctx.appInfo.ownerHandle;
|
|
94
|
+
const items = [{ id: "stripe-key", label: "Stripe secret key", done: Boolean(ctx.secrets.STRIPE_SECRET_KEY) }];
|
|
95
|
+
const ready = items.every((item) => item.done);
|
|
96
|
+
if (!owner) return json({ ok: true, ready, owner });
|
|
97
|
+
return json({ ok: true, ready, owner, items, ownerHandle: ctx.appInfo.ownerHandle, appSlug: ctx.appInfo.appSlug });
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**What the page shows, in four states.** Not done, owner: a **Setup** entry in the app's own navigation, highlighted while incomplete (an app with no navigation shows a top banner that opens it), and the Setup screen lists each item with its linked steps. Not done, non-owner: the feature reads as *not open yet*, nothing else. Done, owner: the Setup entry stays, quiet and reachable — later it is where a key is rotated or test turns into live. Done, non-owner: invisible. The owner's steps live on the Setup surface and nowhere else, and a feature that depends on them reads as not open yet in the consumer flow. The consumer flow is the buyer's, the reader's, the listener's, the same page for everyone; a coffee-order page whose step 3, Payment, turned into the owner's three-link chore is the shape this replaces. The page reads the verdict on mount and again whenever it regains focus or becomes visible, so the feature unlocks in place with no reload — the steps open Settings and the provider in other tabs, and the owner comes back to a feature that works.
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
// App.jsx — one hook, one surface, the verdict decides what each person sees
|
|
105
|
+
// (imports: useEffect, useRef, useState from react; apiFetch from use-vibes; pouf components)
|
|
106
|
+
function useSetup() {
|
|
107
|
+
const [setup, setSetup] = useState(null);
|
|
108
|
+
const latest = useRef(0);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
// Mount, focus and visibility reads overlap; only the latest one may land, or a slow stale
|
|
111
|
+
// answer overwrites a fresh one.
|
|
112
|
+
const load = () => {
|
|
113
|
+
const n = ++latest.current;
|
|
114
|
+
apiFetch("/setup").then((r) => r.json()).catch(() => ({ ready: false, owner: false })).then((s) => { if (n === latest.current) setSetup(s); });
|
|
115
|
+
};
|
|
116
|
+
load();
|
|
117
|
+
const onVisible = () => { if (document.visibilityState === "visible") load(); };
|
|
118
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
119
|
+
window.addEventListener("focus", load);
|
|
120
|
+
return () => { document.removeEventListener("visibilitychange", onVisible); window.removeEventListener("focus", load); };
|
|
121
|
+
}, []);
|
|
122
|
+
return setup;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function SetupScreen({ setup }) {
|
|
126
|
+
const vibe = `${setup.ownerHandle}/${setup.appSlug}`;
|
|
127
|
+
return (
|
|
128
|
+
<Stack>
|
|
129
|
+
<Heading>Setup</Heading>
|
|
130
|
+
{setup.items.map((item) => (
|
|
131
|
+
<Stack key={item.id}>
|
|
132
|
+
<Row gap={2}><Text>{item.label}</Text><Badge tone={item.done ? "up" : "info"}>{item.done ? "Done" : "To do"}</Badge></Row>
|
|
133
|
+
{/* the skill's steps: each one a link to the exact page, in that page's own words */}
|
|
134
|
+
<ol>
|
|
135
|
+
<li>Open Stripe's <a href="https://dashboard.stripe.com/test/apikeys" target="_blank" rel="noreferrer">API keys page (test mode)</a> and press <b>Reveal test key</b> beside <b>Secret key</b>.</li>
|
|
136
|
+
<li>Open <a href={`https://vibes.diy/vibes/mine/${vibe}/settings`} target="_blank" rel="noreferrer">this vibe's Settings tab</a>; under <b>Secrets</b>, enter <code>STRIPE_SECRET_KEY</code>, paste the value, and save.</li>
|
|
137
|
+
</ol>
|
|
138
|
+
</Stack>
|
|
139
|
+
))}
|
|
140
|
+
</Stack>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default function App() {
|
|
145
|
+
const setup = useSetup();
|
|
146
|
+
const [screen, setScreen] = useState("home");
|
|
147
|
+
return (
|
|
148
|
+
<Stack>
|
|
149
|
+
<Row gap={2}>
|
|
150
|
+
<Button variant="quiet" onClick={() => setScreen("home")}>Home</Button>
|
|
151
|
+
{setup?.owner && <Button variant={setup.ready ? "quiet" : "solid"} onClick={() => setScreen("setup")}>Setup{setup.ready ? "" : " · to do"}</Button>}
|
|
152
|
+
</Row>
|
|
153
|
+
{screen === "setup" && setup?.owner ? <SetupScreen setup={setup} /> : setup?.ready ? <PayButton /> : <Text muted>Checkout is not open yet.</Text>}
|
|
154
|
+
</Stack>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Not the same thing: a *visitor's* first-run answers — who are you, which team — are per-person onboarding (the pouf onboarding block), saved as that person's own document, and stay as they are. Owner setup is the app's, done once, and lives on the Setup surface.
|
|
160
|
+
|
|
84
161
|
## The server is still the authority
|
|
85
162
|
|
|
86
163
|
`can.*` is a fast preview, not the final word. A write can still be refused server-side (the server compares the change with the saved record, and the source may be stale, async, or unevaluable — in which case `can.*` optimistically returns `ok` and defers to the server). Write optimistically: `put()` saves locally at once and never rejects for access. A refused write converges to the server's version on its own, and the platform tells the visitor, so most apps need nothing more. For feedback beside the control, read the put's receipt (`const { synced } = await database.put(doc)`, then `synced.on(...)`), or `doc._synced.status` on a row — see the fireproof skill.
|