@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/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. Bring your own Stripe key, and explain how
39
+ ## 1. The setup item: the owner's Stripe secret key
40
40
 
41
- The owner gets a key from Stripe, once. The steps are written for someone
42
- who has never opened Stripe, so each one is a link to the exact page and uses
43
- the words that page uses:
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. To go live, take the **live** secret key from
58
- <https://dashboard.stripe.com/apikeys> (**Reveal live key**, `sk_live_`)
59
- and set it the same way; setting an existing key replaces it.
60
-
61
- The app carries those steps in its own UI, because the person reading them is
62
- the person who has to do them — and it fills in the vibe's real handle and
63
- slug, which the backend knows as `ctx.appInfo`, so the Settings link and the
64
- terminal command are copy-ready rather than templates. The Settings tab opens
65
- in another tab, so the app re-reads `/status` whenever it regains focus or
66
- becomes visible again: the owner saves the key, comes back, and the card gives
67
- way to the Pay buttons without a reload.
68
-
69
- When the key is missing, the app shows the owner the setup steps for a Stripe secret key instead of an error, and the steps link to Stripe's API keys page and to this vibe's own Settings tab by URL.
70
-
71
- The backend answers `{ ok: false, reason: "missing-key" }` when
72
- `ctx.secrets.STRIPE_SECRET_KEY` is absent, and `/status` carries
73
- `ownerHandle` and `appSlug` so the client can render the numbered steps
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
- // Is Stripe set up? The owner gets the steps; everyone else gets a sentence.
312
- if (url.pathname === "/status" && request.method === "GET") {
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
- // ownerHandle + appSlug let the setup card link this vibe's own Settings tab.
315
- return json({ ok: true, configured: Boolean(ctx.secrets.STRIPE_SECRET_KEY), owner, ownerHandle: ctx.appInfo.ownerHandle, appSlug: ctx.appInfo.appSlug });
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 owner's setup card: every step is a link to the exact page, in that page's own words.
430
- function SetupSteps({ status }) {
431
- const vibe = `${status.ownerHandle}/${status.appSlug}`;
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
- <ol>
435
- <li>
436
- Open Stripe's <a href="https://dashboard.stripe.com/test/apikeys" target="_blank" rel="noreferrer">API keys page (test mode)</a>,
437
- 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>.
438
- Copy it; it starts with <code>sk_test_</code>.
439
- </li>
440
- <li>
441
- Open <a href={settingsUrl} target="_blank" rel="noreferrer">this vibe's Settings tab</a> (My Vibes → this vibe → Settings).
442
- Under <b>Secrets</b>, enter the key <code>STRIPE_SECRET_KEY</code>, paste the value, and save.
443
- From a terminal instead: <code>npx vibes-diy secrets set STRIPE_SECRET_KEY --vibe {vibe}</code>.
444
- </li>
445
- <li>
446
- Come back here: the Pay buttons work as soon as the key is saved. In test mode the card 4242 4242 4242 4242 pays.
447
- To go live, do the same with the <a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noreferrer">live key</a>.
448
- </li>
449
- </ol>
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 [status, setStatus] = useState(null); // { configured, owner, ownerHandle, appSlug } from the backend
497
+ const setup = useSetup();
498
+ const [screen, setScreen] = useState("membership");
458
499
  const [error, setError] = useState(null);
459
500
 
460
- // Read status on mount, and again whenever the app comes back into view: the owner sets the key
461
- // in another tab and returns, and the setup card has to give way to the buttons without a reload.
462
- useEffect(() => {
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 configured = status?.configured === true;
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
- <Heading>Membership</Heading>
525
- {paid ? (
526
- <Badge tone="up">Dues paid</Badge>
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
- <Button onClick={() => pay("dues")} disabled={!configured || busy} loading={busy}>
529
- Pay $40 dues — Stripe opens in a new tab
530
- </Button>
531
- )}
532
- {error && <Text>{error}</Text>}
533
- {status?.owner && (
534
- <ul>
535
- {orders.map((o) => (
536
- <li key={o._id}>@{o.buyerHandle} · {o.title} · {(o.amount / 100).toFixed(2)} {o.currency.toUpperCase()} · {o.status}</li>
537
- ))}
538
- </ul>
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.