@homie-rocks/studio 0.50.7 → 0.51.1
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/CHANGELOG.md +54 -0
- package/app-starters/giving/README.md +25 -2
- package/app-starters/giving/app.json +16 -49
- package/app-starters/giving/index.html +1 -1
- package/app-starters/giving/src/rules.ts +1 -1
- package/app-starters/giving/src/view.ts +57 -59
- package/apps/APPS.md +25 -1
- package/apps/apps.ts +1 -1
- package/bin/homie-studio.mjs +5 -1
- package/guides/app/SKILL.md +13 -1
- package/guides/game/RULES.md +26 -0
- package/guides/shop/SKILL.md +41 -20
- package/lib/dev-stripe-fixture.mjs +37 -0
- package/lib/dev-stripe-sandbox.mjs +80 -0
- package/lib/mcp-tools.mjs +15 -1
- package/lib/scaffold.mjs +15 -1
- package/lib/shop-links.mjs +79 -7
- package/lib/shop.mjs +4 -2
- package/lib/stripe-connect.mjs +1 -0
- package/lib/template-history.json +84 -0
- package/package.json +1 -1
- package/rules/correction-path.ts +35 -0
- package/rules/map-index.ts +4 -4
- package/rules/view.ts +196 -28
- package/shop/SELLING.md +2 -2
- package/shop/SHOP.md +120 -5
- package/shop/shop.ts +29 -0
- package/worker/app-format.mjs +6 -0
- package/worker/app-records.mjs +23 -1
- package/worker/booking-store.mjs +79 -0
- package/worker/build-id.mjs +1 -1
- package/worker/index.mjs +5 -2
- package/worker/records-page.mjs +4 -3
- package/worker/shop-links.mjs +3 -2
- package/worker/shop-page.mjs +25 -3
- package/worker/shop-rules.mjs +2 -1
- package/worker/shop-store.mjs +1 -1
- package/worker/shop-subscriptions.mjs +198 -0
- package/worker/shop.mjs +194 -29
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,60 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.51.1] - 2026-10-14
|
|
21
|
+
|
|
22
|
+
**Plugin 0.51.1** · A player's own runner never jumps across the screen.
|
|
23
|
+
|
|
24
|
+
Your platformer keeps a player's runner moving smoothly through network corrections and reconnects, with less work per frame on slower phones.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- In server-rules platformers, a player's own box-shaped 2D runner now moves smoothly through network corrections, even after a delayed frame. It goes around blocking platforms without being pulled backwards against the player's held direction, and normal jumps keep their shape.
|
|
29
|
+
- After a dropped connection, the runner glides from its last visible position to where the server moved it. Real respawns and other deliberate placements still happen immediately; games can identify placements made during an outage with `respawned`.
|
|
30
|
+
- Rules games do less repeated movement, collision and drawing preparation each frame, helping play stay smooth on slower phones. These changes preserve the server's movement, hits and scoring.
|
|
31
|
+
|
|
32
|
+
## [0.51.0] - 2026-10-14
|
|
33
|
+
|
|
34
|
+
**Plugin 0.51.0** · Take the money through your own Stripe, keyless by default.
|
|
35
|
+
|
|
36
|
+
One Stripe approval enables mixed menus, paid capacity bookings and monthly giving through the owner’s own account.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Mixed-item Stripe menus with adjustable quantities, signed paid invoice lines
|
|
41
|
+
and app references such as table 7 on `order.paid`.
|
|
42
|
+
- Booking collections with pending/confirmed/released records and versioned staff
|
|
43
|
+
moves. Each published slot has its own Stripe Payment Link with a completed-session
|
|
44
|
+
capacity limit; the owner's AI syncs slots with `stripe_bookings_sync`.
|
|
45
|
+
- Recurring Payment Links for monthly giving and memberships, paid invoice periods,
|
|
46
|
+
subscription lifecycle events and an API-configured no-code customer portal login.
|
|
47
|
+
- Real sandbox acceptance for menus, two guests with the last-seat link open,
|
|
48
|
+
full-link deactivation, monthly payment, portal, and clock-driven renewal/failure.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- The one-approval CLI connection remains keyless. Homie has no platform credential,
|
|
53
|
+
backend or cut in the payment path. A key alone never selects session checkout;
|
|
54
|
+
`STRIPE_CHECKOUT_MODE=sessions` explicitly opts into the fuller connection.
|
|
55
|
+
- Keyless unpaid bookings are pending, not exclusive holds. Payment secures the seat.
|
|
56
|
+
The optional keyed path retains exclusive expiring holds and exact app-built carts.
|
|
57
|
+
- The giving starter opens real Checkout for suggested one-time/monthly gifts and
|
|
58
|
+
disables payment before connection. It never counts intentions as gifts.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- Records-only app shells include the shop bridge without needing a multiplayer room.
|
|
63
|
+
- Signed menu invoices and Checkout events tolerate either arrival order, and paid
|
|
64
|
+
membership access never extends on a failed renewal or revives after cancellation.
|
|
65
|
+
|
|
66
|
+
### Upgrade notes
|
|
67
|
+
|
|
68
|
+
Existing studios keep their pinned version until they opt in. Apply booking and
|
|
69
|
+
subscription migrations when adding those capabilities. Run connect after changing
|
|
70
|
+
shop items; the owner's AI syncs published slot manifests after schedule changes.
|
|
71
|
+
Stripe's Payment Link and invoice snapshot limits split large catalogues into menus;
|
|
72
|
+
custom amounts and monthly presets use separate links. No studio catalogue cap is added.
|
|
73
|
+
|
|
20
74
|
## [0.50.7] - 2026-10-14
|
|
21
75
|
|
|
22
76
|
**Plugin 0.50.7** · Prove your needs on every screen, with fixes from studio testers.
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Together we can
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This starter opens real Checkout for one-time or monthly gifts from your studio's
|
|
4
|
+
shop. Run `shop init --giving --currency CAD`, then connect the organisation's
|
|
5
|
+
Stripe. The default gift options include $20 monthly. Existing studios and apps
|
|
6
|
+
are unchanged; generate this starter to opt in.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
The phone reads the actual shop's donation options and availability. Without
|
|
9
|
+
Stripe, the options are a disabled preview (`screen.shopPreview: true`). A chosen-amount item (`price: 'choose'`)
|
|
10
|
+
shows an amount field using the item's minimum and maximum. See `shop/SHOP.md`
|
|
11
|
+
for the keyless Payment Links setup and “give $20 monthly” example.
|
|
12
|
+
|
|
13
|
+
Checkout opening never increments a total or creates an intention record. Stripe's
|
|
14
|
+
verified payment supplies `order.paid` and `subscription.started`; subsequent
|
|
15
|
+
invoices supply `subscription.renewed` or `subscription.failed`. Connect these
|
|
16
|
+
events to your own public progress display if desired. The starter deliberately
|
|
17
|
+
shows no collected total until you implement that verified projection.
|
|
18
|
+
|
|
19
|
+
The Manage my gifts link opens the studio shop, where the giver can open their
|
|
20
|
+
own Stripe customer portal. Connect creates the no-code portal login through Stripe's API; no Dashboard toggle is needed.
|
|
21
|
+
Set `giving.receiptText` in shop.json for your organisation's receipt wording.
|
|
22
|
+
Edit app.json name, blurb and colour for your cause. Phone: /<id>/open; wall:
|
|
23
|
+
/<id>/tv. Capture and check both surfaces before sharing.
|
|
24
|
+
|
|
25
|
+
Add the cause’s actual needs to its CODEX.md before running `check`. This starter
|
|
26
|
+
has no pretend shared-record action for the checker to click. For a disconnected
|
|
27
|
+
preview, assert `[data-action]:disabled` on the phone; verify paid flows with the
|
|
28
|
+
Checkout harness and signed events once the keyless connection is available.
|
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "giving",
|
|
3
3
|
"name": "Together we can",
|
|
4
|
-
"blurb": "Support our
|
|
4
|
+
"blurb": "Support our work with a one-time or monthly gift, paid securely through Stripe.",
|
|
5
5
|
"entry": "src/view.ts",
|
|
6
6
|
"roles": {
|
|
7
7
|
"customer": {
|
|
8
8
|
"signIn": false,
|
|
9
|
-
"can": [
|
|
10
|
-
"read:entries",
|
|
11
|
-
"create:entries"
|
|
12
|
-
]
|
|
9
|
+
"can": []
|
|
13
10
|
},
|
|
14
11
|
"wall": {
|
|
15
12
|
"signIn": false,
|
|
16
|
-
"can": [
|
|
17
|
-
"read:entries"
|
|
18
|
-
]
|
|
13
|
+
"can": []
|
|
19
14
|
},
|
|
20
15
|
"staff": {
|
|
21
16
|
"signIn": true,
|
|
22
|
-
"can": [
|
|
23
|
-
"read:entries",
|
|
24
|
-
"update:entries",
|
|
25
|
-
"delete:entries"
|
|
26
|
-
]
|
|
17
|
+
"can": []
|
|
27
18
|
}
|
|
28
19
|
},
|
|
29
20
|
"surfaces": {
|
|
@@ -37,14 +28,14 @@
|
|
|
37
28
|
"join": "Scan to join",
|
|
38
29
|
"one": "person",
|
|
39
30
|
"many": "people",
|
|
40
|
-
"action": "
|
|
41
|
-
"total": "
|
|
31
|
+
"action": "Give on Stripe",
|
|
32
|
+
"total": "Your gift",
|
|
42
33
|
"choice": "Choose an option",
|
|
43
34
|
"monthly": "Gift frequency",
|
|
44
35
|
"once": "One time",
|
|
45
36
|
"month": "Monthly",
|
|
46
37
|
"give": "Give securely on Stripe",
|
|
47
|
-
"goal": "
|
|
38
|
+
"goal": "Our goal",
|
|
48
39
|
"prompt": "Choose what works for you.",
|
|
49
40
|
"finished": "All three answers are in. Thank you!",
|
|
50
41
|
"complete": "Mark complete",
|
|
@@ -54,33 +45,6 @@
|
|
|
54
45
|
"live": "Together \u00b7 live",
|
|
55
46
|
"reconnecting": "Reconnecting\u2026"
|
|
56
47
|
},
|
|
57
|
-
"records": {
|
|
58
|
-
"persist": true,
|
|
59
|
-
"collections": {
|
|
60
|
-
"entries": {
|
|
61
|
-
"fields": {
|
|
62
|
-
"choice": {
|
|
63
|
-
"type": "string",
|
|
64
|
-
"required": true,
|
|
65
|
-
"maxLength": 100
|
|
66
|
-
},
|
|
67
|
-
"status": {
|
|
68
|
-
"type": "string",
|
|
69
|
-
"required": true,
|
|
70
|
-
"maxLength": 60
|
|
71
|
-
},
|
|
72
|
-
"amount": {
|
|
73
|
-
"type": "number",
|
|
74
|
-
"required": true
|
|
75
|
-
},
|
|
76
|
-
"monthly": {
|
|
77
|
-
"type": "boolean",
|
|
78
|
-
"required": true
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
48
|
"parts": [],
|
|
85
49
|
"netplay": {
|
|
86
50
|
"v": 1,
|
|
@@ -100,7 +64,8 @@
|
|
|
100
64
|
"watch": "overview",
|
|
101
65
|
"screen": {
|
|
102
66
|
"join": "bottom-right",
|
|
103
|
-
"share": "top-right"
|
|
67
|
+
"share": "top-right",
|
|
68
|
+
"shopPreview": true
|
|
104
69
|
},
|
|
105
70
|
"needs": {
|
|
106
71
|
"three": "0.185.1",
|
|
@@ -108,12 +73,9 @@
|
|
|
108
73
|
"@homie-rocks/geom": "0.1.1",
|
|
109
74
|
"@types/three": "0.185.1"
|
|
110
75
|
},
|
|
111
|
-
"check": {
|
|
112
|
-
"action": "[data-action]",
|
|
113
|
-
"observe": "[data-count]"
|
|
114
|
-
},
|
|
76
|
+
"check": {},
|
|
115
77
|
"landing": {
|
|
116
|
-
"pitch": "Support our
|
|
78
|
+
"pitch": "Support our work with a one-time or monthly gift, paid securely through Stripe."
|
|
117
79
|
},
|
|
118
80
|
"room": {
|
|
119
81
|
"host": "server"
|
|
@@ -126,6 +88,7 @@
|
|
|
126
88
|
"kind": "giving",
|
|
127
89
|
"choices": [
|
|
128
90
|
"10",
|
|
91
|
+
"20",
|
|
129
92
|
"25",
|
|
130
93
|
"50"
|
|
131
94
|
],
|
|
@@ -158,5 +121,9 @@
|
|
|
158
121
|
]
|
|
159
122
|
}
|
|
160
123
|
]
|
|
124
|
+
},
|
|
125
|
+
"records": {
|
|
126
|
+
"persist": false,
|
|
127
|
+
"collections": {}
|
|
161
128
|
}
|
|
162
129
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Live together</title><style>
|
|
2
|
-
*{box-sizing:border-box}body{margin:0;background:var(--tone,#183b55);color:#fff7df;font-family:system-ui,sans-serif}main{min-height:100svh;padding:clamp(24px,5vw,64px);display:grid;grid-template-columns:1.1fr 1fr;gap:40px;align-items:center}h1{font-size:clamp(44px,6vw,88px);line-height:1;letter-spacing:-.05em;margin:20px 0}p{line-height:1.6;max-width:40em}#connection{font-size:12px;text-transform:uppercase;letter-spacing:.18em}.panel{background:#fff7df;color:#20352e;border-radius:28px;padding:28px;box-shadow:0 30px 80px #0003}#count{font-size:80px;font-weight:800;line-height:1}ul{padding:0;list-style:none}li{padding:12px 0;border-bottom:1px solid #20352e22}button,select,a{font:inherit;min-height:48px;border-radius:12px}button,select{padding:10px 16px;width:100%;margin-top:8px}button{background:var(--tone);color:#fff7df;border:0;cursor:pointer}button:disabled{opacity:.5}a{display:inline-flex;align-items:center;color:inherit}label{display:block;margin-top:14px}select{background:white;color:#20352e;border:1px solid #20352e66}progress{width:100%;height:22px;accent-color:var(--tone)}#status{min-height:24px}#amount{font-size:40px;font-weight:700}button:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid #df9325;outline-offset:4px}.wall #controls{display:none}#giving-link[hidden],[hidden]{display:none!important}@media(max-width:650px){main{display:block;padding:72px 22px 24px}h1{font-size:46px}.panel{margin-top:24px;padding:22px}#count{font-size:
|
|
2
|
+
*{box-sizing:border-box}body{margin:0;background:var(--tone,#183b55);color:#fff7df;font-family:system-ui,sans-serif}main{min-height:100svh;padding:clamp(24px,5vw,64px);display:grid;grid-template-columns:1.1fr 1fr;gap:40px;align-items:center}h1{font-size:clamp(44px,6vw,88px);line-height:1;letter-spacing:-.05em;margin:20px 0}p{line-height:1.6;max-width:40em}#connection{font-size:12px;text-transform:uppercase;letter-spacing:.18em}.panel{background:#fff7df;color:#20352e;border-radius:28px;padding:28px;box-shadow:0 30px 80px #0003}#count{font-size:80px;font-weight:800;line-height:1}ul{padding:0;list-style:none}li{padding:12px 0;border-bottom:1px solid #20352e22}button,select,input,a{font:inherit;min-height:48px;border-radius:12px}button,select,input{padding:10px 16px;width:100%;margin-top:8px}button{background:var(--tone);color:#fff7df;border:0;cursor:pointer}button:disabled{opacity:.5}a{display:inline-flex;align-items:center;color:inherit}label{display:block;margin-top:14px}select,input{background:white;color:#20352e;border:1px solid #20352e66}progress{width:100%;height:22px;accent-color:var(--tone)}#status{min-height:24px}#amount{font-size:40px;font-weight:700}button:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid #df9325;outline-offset:4px}.wall #controls{display:none}#giving-link[hidden],[hidden]{display:none!important}@media(max-width:650px){main{display:block;padding:72px 22px 24px}h1{font-size:46px}.panel{margin-top:24px;padding:22px}#count{font-size:clamp(28px,9vw,46px)}li{font-size:14px}}@media(max-height:520px) and (min-width:651px){main{padding:65px 25px 20px;gap:22px}h1{font-size:38px}.panel{padding:18px}}
|
|
3
3
|
</style><main><header><span id="connection"></span><h1></h1><p id="intro"></p><div id="goal"><p id="goal-label"></p><div><span id="amount"></span> / <span id="target"></span></div><progress></progress></div><ul id="entries" aria-live="polite"></ul></header><section class="panel"><div id="label"></div><div id="count" data-count>0</div><div id="controls"><p id="prompt"></p><label><span id="choice-label"></span><select id="choice"></select></label><label id="frequency"><span id="frequency-label"></span><select id="monthly"><option id="once" value="once"></option><option id="month" value="month"></option></select></label><button data-action></button><a id="giving-link" href="/shop/" target="_blank" rel="noopener"></a></div><p id="status" role="status"></p></section></main><script type="module" src="./assets/main.js"></script></html>
|
|
@@ -15,7 +15,7 @@ export default defineRules({
|
|
|
15
15
|
},
|
|
16
16
|
room: {
|
|
17
17
|
rounds: { seconds: 0, breakSeconds: 0 }, bots: { keep: 0 },
|
|
18
|
-
join() { return { kind: 'participant', at: { x: 0, y: 0 } }; },
|
|
18
|
+
join() { return { kind: 'participant', at: { x: 0, y: 0, z: 0 } }; },
|
|
19
19
|
on: { refresh(world) { world.shared.revision += 1; world.emit('refresh', { x: 0, y: 0 }, {}); } },
|
|
20
20
|
},
|
|
21
21
|
});
|
|
@@ -1,74 +1,72 @@
|
|
|
1
1
|
import app from '../app.json';
|
|
2
2
|
import { openRoom } from '@homie-rocks/studio/rules/view';
|
|
3
3
|
import type rules from './rules';
|
|
4
|
-
import {
|
|
5
|
-
import { randomId } from '@homie-rocks/studio/links';
|
|
6
|
-
type Entry = { choice: string; status: string; amount: number; monthly: boolean };
|
|
4
|
+
import { createShop, type ShopCatalog } from '@homie-rocks/studio/shop';
|
|
7
5
|
const room = openRoom<typeof rules>();
|
|
8
|
-
const
|
|
9
|
-
const role = net.params.role ?? 'customer';
|
|
10
|
-
const words: Record<string, string> = app.words;
|
|
11
|
-
const config = app.starter;
|
|
6
|
+
const shop = createShop();
|
|
12
7
|
const $ = <T extends HTMLElement = HTMLElement>(s: string) => document.querySelector<T>(s)!;
|
|
13
|
-
const records = createAppRecords<Entry>({ collection: 'entries' });
|
|
14
|
-
let rows: AppRecord<Entry>[] = [], busy = false, ready = false, question = 0;
|
|
15
8
|
const action = $<HTMLButtonElement>('[data-action]');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
$('
|
|
23
|
-
$('#
|
|
24
|
-
$('#
|
|
25
|
-
$('#
|
|
26
|
-
$('#
|
|
27
|
-
$('#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
choices();
|
|
9
|
+
const choice = $<HTMLSelectElement>('#choice');
|
|
10
|
+
const frequency = $<HTMLSelectElement>('#monthly');
|
|
11
|
+
let catalog: ShopCatalog | undefined, busy = false;
|
|
12
|
+
const wall = room.net.params.role === 'wall';
|
|
13
|
+
document.body.className = room.net.params.role ?? 'customer';
|
|
14
|
+
document.body.style.setProperty('--tone', app.starter.color);
|
|
15
|
+
$('h1').textContent = app.name;
|
|
16
|
+
$('#intro').textContent = app.blurb;
|
|
17
|
+
$('#label').textContent = wall ? 'Give with us' : 'Your gift';
|
|
18
|
+
$('#choice-label').textContent = 'Choose a gift';
|
|
19
|
+
$('#frequency-label').textContent = 'Gift frequency';
|
|
20
|
+
$('#once').textContent = 'One time'; $('#month').textContent = 'Monthly';
|
|
21
|
+
$('#giving-link').textContent = 'Manage my gifts';
|
|
22
|
+
$('#goal').hidden = true;
|
|
23
|
+
$('#entries').hidden = true;
|
|
24
|
+
$('#prompt').textContent = 'Choose what works for you. Payments go directly to our Stripe account.';
|
|
25
|
+
const amountLabel = document.createElement('label'); amountLabel.textContent = 'Your amount';
|
|
26
|
+
const amount = document.createElement('input'); amount.type = 'number'; amount.inputMode = 'decimal'; amountLabel.append(amount);
|
|
27
|
+
choice.parentElement!.after(amountLabel);
|
|
28
|
+
function selected() { return catalog?.items.find(i => i.id === choice.value); }
|
|
37
29
|
function paint() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
li.textContent = `${row.data.choice} · ${row.data.status}${row.data.monthly ? ' · ' + words.month : ''}`;
|
|
49
|
-
if (role === 'staff' && row.data.status !== words.done) {
|
|
50
|
-
const button = document.createElement('button'); button.textContent = words.complete;
|
|
51
|
-
button.onclick = () => { void records.update(row, { ...row.data, status: words.done }).then(refresh).catch(e => { $('#status').textContent = e.message; }); }; li.append(button);
|
|
52
|
-
}
|
|
53
|
-
$('#entries').append(li);
|
|
30
|
+
const item = selected();
|
|
31
|
+
amountLabel.hidden = item?.price !== 'choose';
|
|
32
|
+
action.disabled = busy || !catalog?.open || !item || item.way === 'closed';
|
|
33
|
+
action.textContent = frequency.value === 'month' ? 'Give monthly on Stripe' : 'Give on Stripe';
|
|
34
|
+
$('[data-count]').textContent = wall ? 'Thank you' : item?.shown ?? 'Preview';
|
|
35
|
+
if (item?.price === 'choose') {
|
|
36
|
+
const scale = catalog!.currencyScale;
|
|
37
|
+
amount.min = String((item.min ?? 0) / scale);
|
|
38
|
+
amount.max = item.max == null ? '' : String(item.max / scale);
|
|
39
|
+
amount.step = String(1 / scale);
|
|
54
40
|
}
|
|
55
41
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
42
|
+
function options() {
|
|
43
|
+
choice.replaceChildren();
|
|
44
|
+
for (const item of catalog?.items ?? []) {
|
|
45
|
+
if (item.kind !== 'donation' || Boolean(item.interval) !== (frequency.value === 'month')) continue;
|
|
46
|
+
const option = document.createElement('option'); option.value = item.id; option.textContent = `${item.name} · ${item.shown}`; choice.append(option);
|
|
47
|
+
}
|
|
48
|
+
paint();
|
|
59
49
|
}
|
|
50
|
+
choice.onchange = paint; frequency.onchange = options;
|
|
60
51
|
action.onclick = async () => {
|
|
52
|
+
const item = selected(); if (!item || !catalog?.open) return;
|
|
53
|
+
const value = item.price === 'choose' ? Math.round(Number(amount.value) * catalog.currencyScale) : undefined;
|
|
54
|
+
if (item.price === 'choose' && (!amount.value || !Number.isSafeInteger(value) || value! < (item.min ?? 0) || item.max != null && value! > item.max)) {
|
|
55
|
+
$('#status').textContent = 'Enter an amount within the displayed range.'; return;
|
|
56
|
+
}
|
|
61
57
|
busy = true; paint();
|
|
62
58
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
$('#status').textContent =
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
} catch (error) { $('#status').textContent = (error as Error).message; }
|
|
69
|
-
finally { busy = false; paint(); }
|
|
59
|
+
// Opening Checkout is not a payment. Only Stripe's verified webhook records a gift.
|
|
60
|
+
const opened = await shop.buy(item.id, 1, value);
|
|
61
|
+
$('#status').textContent = opened ? 'Finish your gift on Stripe. Your receipt confirms payment.' : 'Open the shop to sign in or complete the steps for this gift.';
|
|
62
|
+
if (!opened) shop.open(item.id);
|
|
63
|
+
} finally { busy = false; paint(); }
|
|
70
64
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
async function refresh() {
|
|
66
|
+
catalog = await shop.catalog(); options();
|
|
67
|
+
$('#status').textContent = wall ? 'Join on your phone to choose a one-time or monthly gift.' : catalog.open ? 'Monthly gifts renew until you cancel through Stripe. Manage your gifts using the link below.' : 'Preview — giving is unavailable until the organisation connects Stripe.';
|
|
68
|
+
$('#connection').textContent = room.net.link === 'online' ? 'Together · live' : 'Connecting…';
|
|
69
|
+
room.net.playable();
|
|
70
|
+
}
|
|
71
|
+
shop.on('closed', () => { void refresh(); });
|
|
74
72
|
void refresh();
|
package/apps/APPS.md
CHANGED
|
@@ -49,7 +49,7 @@ Parts import exactly as games do (`@parts/<id>` or `@parts/<host>/<id>`), with t
|
|
|
49
49
|
|
|
50
50
|
## Shop button on the screen
|
|
51
51
|
|
|
52
|
-
An open shop with items for this game or app shows **Shop** above its play/open screen. The shell reserves 52 pixels plus the safe area above the content, so the button never covers its controls. In `game.json` or `app.json`, set `"screen": { "shop": false }` to
|
|
52
|
+
An open shop with items for this game or app shows **Shop** above its play/open screen. The shell reserves 52 pixels plus the safe area above the content, so the button never covers its controls. In `game.json` or `app.json`, set `"screen": { "shop": false }` to disable the experience’s shop (merge with any existing screen settings). Keep it true or omitted when your app uses its own `createShop` buttons; false also refuses their checkout bridge requests. Closed shops, kids policies and an empty selection show no button.
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
55
|
import { createShop } from '@homie-rocks/studio/shop';
|
|
@@ -103,3 +103,27 @@ and the `app_records` tool schema explain the contract; validation errors name i
|
|
|
103
103
|
Live and local unconnected shop/giving pages display the declared items and amounts
|
|
104
104
|
with disabled payment controls. They say they are not taking payments yet. A fresh
|
|
105
105
|
`/shop/` has an empty preview; an authored shop page still takes precedence.
|
|
106
|
+
|
|
107
|
+
## Booking capacity
|
|
108
|
+
|
|
109
|
+
See `shop/SHOP.md` for “book a tune and hold the slot”. A collection opts in with
|
|
110
|
+
`booking: {slots: 'slots', item: 'full-tune'}`. Slots are ordinary staff-managed
|
|
111
|
+
records with integer `capacity`. Bookings are server-managed records created by
|
|
112
|
+
`shop.book`; their public data is only slot, quantity and state. Keep personal
|
|
113
|
+
information in a separate private collection. Public roles must not have write
|
|
114
|
+
capabilities for slots or bookings. Staff movement is a versioned record update
|
|
115
|
+
containing only `slot`; confirmation and release follow verified Checkout state.
|
|
116
|
+
The default keyless connection uses one Payment Link per published slot. Call
|
|
117
|
+
`stripe_bookings_sync` after publishing/changing slots. Pending checkout does not
|
|
118
|
+
block others; payment secures the seat. Use record.booking.full/available/synced
|
|
119
|
+
for slot display. Read SHOP.md for the complete manifest and limitations.
|
|
120
|
+
|
|
121
|
+
The giving starter now reads the actual shop donation options with `shop.catalog()`.
|
|
122
|
+
`shop init --giving --currency CAD` includes $20 monthly; `app new giving --from giving`
|
|
123
|
+
opens Checkout for the selected amount and frequency. A disconnected shop displays
|
|
124
|
+
a disabled preview. No intention record or unverified collected total is created.
|
|
125
|
+
Use the Manage my gifts link for the buyer-owned Stripe portal in the shop.
|
|
126
|
+
|
|
127
|
+
Records-only apps support the same shop bridge as room apps. In the sandboxed
|
|
128
|
+
view, use `createShop({target: parent})` to send checkout and booking requests to
|
|
129
|
+
the authenticated outer shell; a room simulation is not required for payments.
|
package/apps/apps.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Lasting records through the same sandbox bridge used by saves and the shop. */
|
|
2
2
|
import { randomId } from '../links/links.mjs';
|
|
3
|
-
export interface AppRecord<T> { id: string; data: T; version: number; updatedAt: number }
|
|
3
|
+
export interface AppRecord<T> { id: string; data: T; version: number; updatedAt: number; booking?: {full: boolean; available: number; synced: boolean} }
|
|
4
4
|
export function createAppRecords<T extends Record<string, unknown>>({ collection }: { collection: string }) {
|
|
5
5
|
if (!/^[a-z][a-z0-9-]{0,39}$/.test(collection)) throw new Error('invalid collection');
|
|
6
6
|
const pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>();
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -788,7 +788,11 @@ function print(result) {
|
|
|
788
788
|
`Written: ${result.file}`);
|
|
789
789
|
break;
|
|
790
790
|
case 'check':
|
|
791
|
-
if (result.kind === 'app') {
|
|
791
|
+
if (result.kind === 'app') {
|
|
792
|
+
if (result.needs?.length) lines.push(`PASS: app ${result.game ?? ''}: ${result.needs.length} Codex needs checked on ${(result.surfaces ?? []).join(', ')}.`);
|
|
793
|
+
if (result.reconnect) lines.push(`PASS: app ${result.game ?? ''}: wall and two phones shared an action; reconnect passed.`, ` ${result.before} → ${result.after}`);
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
792
796
|
if (result.results) { for (const r of result.results) lines.push(`${r.game}: PASS at ${r.site}: room ${r.room}, round ${r.round?.n} (${r.round?.humans} humans, ${r.round?.bots} bots), connection ${r.uninterrupted === true ? 'uninterrupted' : r.uninterrupted === false ? 'INTERRUPTED' : 'unknown'}`); break; }
|
|
793
797
|
if (result.site) lines.push(`Checked ${result.local ? 'this studio\'s own build' : 'the site'} at ${result.site}.`);
|
|
794
798
|
lines.push(`PASS: two fresh browsers in room ${result.room} finished round ${result.round.n} (${result.round.humans} humans, ${result.round.bots} bots) in ${Math.round(result.totalMs / 1000)} s.`,
|
package/guides/app/SKILL.md
CHANGED
|
@@ -108,6 +108,8 @@ await shop.buy('full-tune', 1, undefined, { reference: booking.id });
|
|
|
108
108
|
await shop.checkout({ reference: booking.id });
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
On the default keyless connection, `shop.checkout({ reference: 'table-7' })` opens the studio's synced Stripe menu: the guest sets quantities on Stripe's page and `order.paid.data.lines` carries what was actually paid. A slot with capacity uses `shop.book` (see below), not `shop.buy`. Monthly gifts and memberships are recurring items on their own links. None of these needs a key.
|
|
112
|
+
|
|
111
113
|
These retain their existing boolean result: true means checkout opened, not payment received. The durable `order.paid` function event includes `data.order` (the order ID), `data.reference` (your reference or null), and `data.app` (the app ID). A verified provider payment emits it. Validate the referenced record, expected amount/currency and app, deduplicate by event/order ID, then fulfil that booking or add that gift to the confirmed meal table. A buyer supplies the reference: it is correlation, never proof they own a private record or paid the expected price. Do not put personal data in it.
|
|
112
114
|
|
|
113
115
|
For each offer, build the promised action and fulfilment, not just a price label: a naming purchase needs the proposed name and the staff/customer outcome; a ticket needs its receipt. Keep an unpaid request distinct when charging is unavailable.
|
|
@@ -175,7 +177,7 @@ Scaffold businesses with `new <folder> --kind business`; causes with `--kind cau
|
|
|
175
177
|
|
|
176
178
|
For giving, set the studio currency and run `shop init --giving --currency CAD` (use the organisation’s currency). It creates one-time/monthly suggested gifts. The owner supplies `giving.receiptText` in shop.json; Homie supplies no legal or tax text. Before Stripe is connected, local /shop/ previews items and prices with checkout disabled. Run the connection step yourself; the owner approves their Stripe account in the browser.
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
Use `shop.book` and the booking collection declaration in SHOP.md. Default keyless booking links show pending while paying; an unpaid guest does not block others. Stripe limits completed sessions to capacity. The owner’s AI syncs published/changed slots with `stripe_bookings_sync`. Say “Pay to secure your seat.” Exclusive unpaid holds require the explicitly selected optional keyed connection.
|
|
179
181
|
|
|
180
182
|
|
|
181
183
|
## Tell my AI when a booking comes in
|
|
@@ -256,3 +258,13 @@ and the `app_records` tool schema explain the contract; validation errors name i
|
|
|
256
258
|
Live and local unconnected shop/giving pages display the declared items and amounts
|
|
257
259
|
with disabled payment controls. They say they are not taking payments yet. A fresh
|
|
258
260
|
`/shop/` has an empty preview; an authored shop page still takes precedence.
|
|
261
|
+
|
|
262
|
+
The giving starter now reads the actual shop donation options with `shop.catalog()`.
|
|
263
|
+
`shop init --giving --currency CAD` includes $20 monthly; `app new giving --from giving`
|
|
264
|
+
opens Checkout for the selected amount and frequency. A disconnected shop displays
|
|
265
|
+
a disabled preview. No intention record or unverified collected total is created.
|
|
266
|
+
Use the Manage my gifts link for the buyer-owned Stripe portal in the shop.
|
|
267
|
+
|
|
268
|
+
For a records-only booking app, use `createShop({target: parent})` inside its
|
|
269
|
+
sandboxed view. The shell includes the authenticated shop bridge; do not add a
|
|
270
|
+
room simulation merely to make a payment button work.
|
package/guides/game/RULES.md
CHANGED
|
@@ -1443,3 +1443,29 @@ catch-up path. It resumes live prediction at once and carries the drawn pose int
|
|
|
1443
1443
|
the normal correction blend, so a counter-dash or jump does not wait for the
|
|
1444
1444
|
unseen impulse's history to finish. This changes drawing only; authority, input
|
|
1445
1445
|
timestamps, cooldowns and replay rules are unchanged.
|
|
1446
|
+
|
|
1447
|
+
Own box-bodied 2D presentation also sweeps from its last displayed position. Reconciliation
|
|
1448
|
+
cannot switch sides of a solid merely because the new predicted origin is on
|
|
1449
|
+
the other side. Travel is limited by elapsed time plus a body-sized blend. Horizontal travel uses the adjacent
|
|
1450
|
+
actual velocities; vertical travel uses the recorded substeps with interpolation headroom, so an apex or landing is not trimmed.
|
|
1451
|
+
A stationary correction spends only blend speed, not the declared terminal falling speed.
|
|
1452
|
+
A large falling-speed limit cannot accelerate a horizontal correction. If sliding cannot reach a correction, a bounded
|
|
1453
|
+
incremental route searches collision-tested edges around the obstruction. No
|
|
1454
|
+
route means holding the last valid position, never crossing a solid. Explicit
|
|
1455
|
+
placements and epoch changes reset presentation immediately. These positions
|
|
1456
|
+
never feed movement, hits, scoring, input timestamps, or authority.
|
|
1457
|
+
|
|
1458
|
+
A platformer can supply `openRoom({ correctionDirection: input => Number(input.r || 0) - Number(input.l || 0) })`
|
|
1459
|
+
for box-bodied 2D presentation. This is horizontal control intent (-1, 0, +1), not authority or a second movement loop.
|
|
1460
|
+
It prevents a visual correction from reversing held controls even when contact sets velocity to zero; neutral input
|
|
1461
|
+
allows convergence in either direction. Actual predicted braking against a newly reversed control remains allowed.
|
|
1462
|
+
|
|
1463
|
+
After a delayed render, box-bodied 2D presentation spends at most 1.4 body widths per drawn frame,
|
|
1464
|
+
then continues toward prediction. This affects drawing only; an explicit placement remains immediate.
|
|
1465
|
+
|
|
1466
|
+
When a player's connection comes back, the server hands the body back where it has it, with a new placement
|
|
1467
|
+
revision. Within three seconds of the link coming back online, box-bodied 2D presentation treats that revision as the
|
|
1468
|
+
same body and draws on from where it was shown, gliding and routing around solids as for any correction. A body that
|
|
1469
|
+
was really placed while the player was away (a respawn, a new round) should still be drawn at once: say so with
|
|
1470
|
+
`openRoom({ respawned: (before, after) => after.diedAt !== before.diedAt })`, where `before` and `after` are the own
|
|
1471
|
+
body as the server last had it before and after the outage.
|
package/guides/shop/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: shop
|
|
3
|
-
description: Sell things in a Homie studio's games with the studio's OWN Stripe - a supporter pack, cosmetics, a season pass, a one-time unlock, a tip - in real money, with Stripe Checkout, Stripe Tax or Stripe Managed Payments, refunds from the office, and optional studio policies with an open default; set up with Stripe's own agent tools (Stripe's MCP server and skills) with one browser approval and Payment Links by default, no human-handled key and no API key in the Worker. Use when someone asks to sell something, add a shop or store, take payments or donations, make money from a game, set up or connect Stripe, make the products in Stripe, asks "how are sales?", about tax or Managed Payments, to add a supporter badge, refund a player, or about chargebacks.
|
|
3
|
+
description: Sell things in a Homie studio's games with the studio's OWN Stripe - a supporter pack, cosmetics, a season pass, a one-time unlock, a tip, a round of several items to a table, a paid booking with slot capacity, monthly giving or a membership - in real money, with Stripe Checkout, Stripe Tax or Stripe Managed Payments, refunds from the office, and optional studio policies with an open default; set up with Stripe's own agent tools (Stripe's MCP server and skills) with one browser approval and Payment Links by default, no human-handled key and no API key in the Worker. Use when someone asks to sell something, add a shop or store, take payments or donations, take paid bookings, set up monthly giving or memberships, make money from a game, set up or connect Stripe, make the products in Stripe, asks "how are sales?", about tax or Managed Payments, to add a supporter badge, refund a player, or about chargebacks.
|
|
4
4
|
compatibility: The studio's own current toolkit with stripe_login or shop connect --renew, Wrangler, and the official Stripe CLI. Stripe's own agent plugin (MCP and skills) is optional for catalog work and analytics.
|
|
5
5
|
metadata:
|
|
6
6
|
providers: stripe
|
|
@@ -43,13 +43,18 @@ The Worker redirects buyers to Stripe and grants purchases from signed events. N
|
|
|
43
43
|
Homie service or API key is needed when someone pays. Run connect again after editing shop.json;
|
|
44
44
|
it updates changed objects and archives removed ones. Existing connections need no new approval.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
Keyless also supports mixed fixed-price menus, capacity-limited booking links and fixed
|
|
47
|
+
monthly gifts/memberships. Read SHOP.md's 0.51 examples before implementing these.
|
|
48
|
+
Guests choose menu quantities on Stripe. Booking links show pending without holding
|
|
49
|
+
an unpaid seat; Stripe's completion limit is the hard capacity. Use
|
|
50
|
+
`stripe_bookings_sync` after publishing/changing slots (complete manifest), or update
|
|
51
|
+
`.studio/booking-slots.json` and rerun connect. Sync creates the no-code portal login
|
|
52
|
+
through the API; no Dashboard toggle is needed. Never ask for a key.
|
|
53
|
+
|
|
54
|
+
Only when explicitly selected, an existing Worker key plus
|
|
55
|
+
`STRIPE_CHECKOUT_MODE=sessions` enables exact app carts, exclusive expiring holds,
|
|
56
|
+
spending reservations, provider reads and runtime refunds. A key alone never selects
|
|
57
|
+
it. Default connect selects links and removes the runtime key.
|
|
53
58
|
|
|
54
59
|
## Never
|
|
55
60
|
|
|
@@ -124,7 +129,7 @@ No toolkit amount or duration ceiling applies. Stripe currency constraints and s
|
|
|
124
129
|
A refund window includes used items unless `policy.refundUsedItems` is false. A player cannot refund a tip;
|
|
125
130
|
the owner can refund through Stripe; keyed shops also offer office refunds. Guest purchases stay owned when the buyer signs in later.
|
|
126
131
|
|
|
127
|
-
For keyed shops, carts: `shop.add(item, quantity, amount?)`, then `shop.checkout()`; `shop.buy(item)` buys directly.
|
|
132
|
+
For menus or explicitly keyed shops, carts: `shop.add(item, quantity, amount?)`, then `shop.checkout()`; `shop.buy(item)` buys directly.
|
|
128
133
|
Free and paid lines share one order and one Stripe Session. The studio chooses where to open the shop.
|
|
129
134
|
|
|
130
135
|
## The first sale (the acceptance)
|
|
@@ -133,17 +138,11 @@ In test mode: buy the supporter pack on a phone with Stripe's test card `4242 42
|
|
|
133
138
|
see the badge on the account page and beside the name in a room (from the next room the player joins), refund it
|
|
134
139
|
in Stripe (or through the connected AI), and see the badge go. With the protective preset, a kids server's room shows no shop and `/<game>/tv` shows only a code.
|
|
135
140
|
|
|
136
|
-
Keyless
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Payment Links are public and reusable; do not promise that local sale dates, repeat-purchase or
|
|
142
|
-
eligibility checks revoke a previously opened link. Deactivation stops new sessions after re-sync;
|
|
143
|
-
already opened sessions may still complete. Strict pre-payment enforcement needs the keyed way.
|
|
144
|
-
Stripe always enables Adaptive Pricing on Payment Links; the buyer can use the original currency.
|
|
145
|
-
Keyless has no per-session expiry or provider-read reconciliation. The keyed path retains both,
|
|
146
|
-
carts, cap reservations, local refunds. Managed Payments is supported on Payment Links when Stripe enables it for the account. The studio chooses its needs.
|
|
141
|
+
Keyless menus offer several fixed-price items with adjustable quantities on Stripe.
|
|
142
|
+
Custom amounts and monthly preset gifts have their own links. Free orders stay local.
|
|
143
|
+
A missing webhook stays pending; ask Stripe to resend it. Never grant from a return
|
|
144
|
+
URL. Keyless has no exclusive unpaid hold or custom session expiry. Consult SHOP.md
|
|
145
|
+
for menu limits, signed invoice fulfilment, slot sync and honest pending/full wording.
|
|
147
146
|
|
|
148
147
|
For a cause, use `shop init --giving --currency <organisation currency>`: one-time and monthly suggested gifts, no badge or game access. Read `giving.receiptText` from the organisation; leave it empty until provided. Local /shop/ previews before Stripe. Monthly gifts use the existing keyless Payment Links connection and Stripe handles renewals.
|
|
149
148
|
|
|
@@ -152,3 +151,25 @@ For a cause, use `shop init --giving --currency <organisation currency>`: one-ti
|
|
|
152
151
|
Create the booking/ticket/gift intention first. `shop.buy('full-tune', 1, undefined, { reference: booking.id })` or `shop.checkout({ reference: booking.id })` sends the app's own reference through checkout. Existing boolean returns are unchanged: checkout opened is not paid. The verified durable `order.paid` event carries `data.order`, `data.reference` (null if omitted) and `data.app`. Reopened Payment Links retain the reference. A reference is buyer-supplied correlation, not authorization: verify the record, app, amount and currency, deduplicate by event/order ID, then fulfil it. Keep personal data out of references. Before Stripe is connected, local prices and unpaid requests can be demonstrated; actual payment and recurring collection remain unverified. Test events must be labelled simulations.
|
|
153
152
|
|
|
154
153
|
For referrals, use Stripe Connect to split payments or a Stripe-based affiliate service; Homie builds none.
|
|
154
|
+
|
|
155
|
+
## Earning with the keyless default
|
|
156
|
+
|
|
157
|
+
Read SHOP.md for a round to table 7, `shop.book` and $20 monthly giving.
|
|
158
|
+
Menus let guests choose several quantities on Stripe; signed invoices provide actual
|
|
159
|
+
paid lines and the app reference. Slot Payment Links enforce completed-session
|
|
160
|
+
capacity. Pending guests do not block others. The owner's AI syncs published slots
|
|
161
|
+
with `stripe_bookings_sync`; the app shows Full from verified availability.
|
|
162
|
+
Recurring Prices provide monthly preset gifts and memberships. Connect enables the
|
|
163
|
+
no-code customer portal login through Stripe's API, without a Dashboard toggle.
|
|
164
|
+
Paid invoice periods govern membership; failed renewal never extends it.
|
|
165
|
+
The giving starter opens real keyless Checkout and shows a disabled preview before
|
|
166
|
+
connection. It never counts an intention as a gift.
|
|
167
|
+
|
|
168
|
+
For real sandbox acceptance use `startKeylessStripeSandbox` from the installed
|
|
169
|
+
`lib/dev-stripe-sandbox.mjs` as documented in SHOP.md. Only its explicitly authorised
|
|
170
|
+
sandbox CLI profile creates Stripe objects. No Worker API key. Keep returned signing
|
|
171
|
+
secret bindings private and temporary. Complete payments with Stripe's published
|
|
172
|
+
CLI fixture/API tokens; never enter card digits into a page. The local listener is
|
|
173
|
+
for development; production sends signed events directly to the studio's webhook.
|
|
174
|
+
The optional keyed fixture remains available for testing explicitly selected session
|
|
175
|
+
checkout. Label offline fixtures simulations.
|