studio-engine 0.20.0 → 0.21.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/app/views/studio/_age_verify_assets.html.erb +120 -0
- data/app/views/studio/modals/blocks/_age_verify.html.erb +106 -0
- data/app/views/studio/modals/blocks/_wallet_brand_sprite.html.erb +15 -26
- data/app/views/style/_modals.html.erb +90 -2
- data/app/views/style/modals/_age_verify.html.erb +17 -0
- data/app/views/style/modals/_entry_tokens.html.erb +123 -0
- data/lib/studio/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14686260b35b4cd72975c3bf156020de61094144f09d8e71f3d6eeb045b5bcc3
|
|
4
|
+
data.tar.gz: 35b5f1bb5cb7f1236d21baa47c3e28d6d7f00420eb775da0719cd9874b29b4b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a8b31f5f092a8998590c47877fb23631316fbaee2ab8bca2199038d27497775db342a5ba4f2b759bd09b2ae44256ad57b857cea5742731f84c59a8525a3b1e3
|
|
7
|
+
data.tar.gz: 506f56fbadf51b53c7a1f779678bab0ba1de8fc485493acb4d3ac8aa19ddfd29c4b103d789107d3f4258b381d709fdefdd00fc7ab81dd8cdfffbc59f8baffb88
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
|
|
4
4
|
|
|
5
|
+
## 0.21.0 — 2026-07-27
|
|
6
|
+
|
|
7
|
+
Phase 2 of the studio-engine modal convergence: homes the entry-time age-gate DOB
|
|
8
|
+
modal as a policy-free engine primitive, and documents the app-specific entry-token
|
|
9
|
+
purchase flow as a living-style-guide specimen.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Age-gate DOB modal — `studio/modals/blocks/_age_verify`.** The entry-time
|
|
14
|
+
date-of-birth gate (Month / Day / Year + a live "too young" hint + submit),
|
|
15
|
+
homed in the engine as the heavier sibling of the existing
|
|
16
|
+
`studio/modals/shared/_age_attestation` checkbox. **Run one or the other:** the
|
|
17
|
+
attestation is a signup-time "I confirm I'm of legal age" checkbox; this is an
|
|
18
|
+
entry-time DOB gate a server can recompute and stamp.
|
|
19
|
+
|
|
20
|
+
**The engine hardcodes NO legal policy** — the whole seam is app-supplied:
|
|
21
|
+
`min_age` (REQUIRED, with **no engine default** — 18 is itself a policy value),
|
|
22
|
+
`submit_url` (the app owns the authoritative recompute + DOB persistence),
|
|
23
|
+
`state` (an optional PASSIVE jurisdiction label — the modal never detects
|
|
24
|
+
geography and never offers an editable state field, so a spoofed client state
|
|
25
|
+
can't lower the bar), and `fine_print` (the app's per-jurisdiction legal copy;
|
|
26
|
+
the engine ships only a neutral, policy-free default). The per-state age table
|
|
27
|
+
that the app's policy encodes never enters the engine.
|
|
28
|
+
|
|
29
|
+
- **`ageVerifyModal` factory — `studio/_age_verify_assets`.** The Alpine data for
|
|
30
|
+
the DOB modal, shipped at page level (a `<script>` cloned out of a modal-host
|
|
31
|
+
template never runs). A consumer renders it once in its layout, like the shared
|
|
32
|
+
alpine factories; mirrors how `studio/_cropper_assets` homes `cropPhotoModal`.
|
|
33
|
+
The factory carries zero policy — no age default, no state table; the client
|
|
34
|
+
age math is UX only, the server recompute stays authoritative.
|
|
35
|
+
|
|
36
|
+
- **Living style guide — new "Eligibility & entry" group.** Adds two specimens to
|
|
37
|
+
the `/admin/style` Modals section:
|
|
38
|
+
- The **age-gate** specimen, opened with a clearly-labelled DEMO policy (21+ in
|
|
39
|
+
CA) that documents the app-supplied seam, gated by
|
|
40
|
+
`Studio.feature?(:age_gate)` — disabled-but-present-yet-openable when off,
|
|
41
|
+
like the web3 specimens.
|
|
42
|
+
- The **entry-tokens** specimen — the app-specific purchase flow
|
|
43
|
+
(picker → confirming → minted) documented by composing engine chrome
|
|
44
|
+
(`shell`, `card_header`, `progress_pill`) with illustrative demo packs. The
|
|
45
|
+
engine does NOT own the packs, pricing, rails, or the on-chain mint. The copy
|
|
46
|
+
reflects the real on-chain model: an entry token is a **prepaid entry credit
|
|
47
|
+
recorded on-chain** (not a wallet-held SPL token), **minted server-side** once
|
|
48
|
+
payment clears and **consumed** when the app's hold-to-confirm creates the
|
|
49
|
+
entry.
|
|
50
|
+
|
|
51
|
+
### Capability gating
|
|
52
|
+
|
|
53
|
+
- **`:age_gate`** — a new, independent capability flag gating the age-gate
|
|
54
|
+
specimen (and the intended consumer mount). Off by default; a consumer opts in
|
|
55
|
+
via `config.features`.
|
|
56
|
+
|
|
57
|
+
### Unchanged
|
|
58
|
+
|
|
59
|
+
- Backward compatible: every addition is additive/optional. Nothing existing
|
|
60
|
+
changes behaviour, and the lighter `_age_attestation` checkbox is untouched.
|
|
61
|
+
|
|
5
62
|
## 0.20.0 — 2026-07-27
|
|
6
63
|
|
|
7
64
|
Phase 1 of the studio-engine modal convergence: the engine now OWNS the wallet
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
ageVerifyModal factory — the Alpine data for the engine age-gate DOB modal
|
|
3
|
+
(studio/modals/blocks/_age_verify). Ships here, at PAGE level, on purpose: the
|
|
4
|
+
modal partial mounts inside a modal host's single-root template, and a script
|
|
5
|
+
cloned out of a template never runs. So a consumer renders THIS once in its
|
|
6
|
+
layout (like the shared alpine factories), and the modal partial anywhere below
|
|
7
|
+
just references window.ageVerifyModal. Mirrors how studio/_cropper_assets homes
|
|
8
|
+
the cropPhotoModal factory for the crop modal.
|
|
9
|
+
|
|
10
|
+
The factory holds ZERO legal policy — no age default, no state table. minAge is
|
|
11
|
+
whatever the app hands it (a required prop upstream); the client "too young"
|
|
12
|
+
hint is UX only, and the server recompute at submit_url stays authoritative.
|
|
13
|
+
|
|
14
|
+
opts: { minAge, state, url, store, demo }.
|
|
15
|
+
minAge app-supplied minimum age (no engine fallback — 0 means unsupplied).
|
|
16
|
+
state passive jurisdiction label (server-detected upstream; never editable).
|
|
17
|
+
url app endpoint the DOB POSTs to (the app owns the authoritative check).
|
|
18
|
+
store Alpine modal store name backing close() (default "modals").
|
|
19
|
+
demo style-guide preview: resolve locally, skip the network POST.
|
|
20
|
+
%>
|
|
21
|
+
<script>
|
|
22
|
+
(function () {
|
|
23
|
+
if (window.ageVerifyModal) return;
|
|
24
|
+
window.ageVerifyModal = function (opts) {
|
|
25
|
+
opts = opts || {};
|
|
26
|
+
var now = new Date();
|
|
27
|
+
return {
|
|
28
|
+
month: "", day: "", year: "",
|
|
29
|
+
submitting: false,
|
|
30
|
+
error: "",
|
|
31
|
+
// No engine age default: 18 is itself a policy value. The app supplies it.
|
|
32
|
+
minAge: parseInt(opts.minAge, 10) || 0,
|
|
33
|
+
stateCode: opts.state || "",
|
|
34
|
+
url: opts.url || "",
|
|
35
|
+
store: opts.store || "modals",
|
|
36
|
+
demo: !!opts.demo,
|
|
37
|
+
months: [
|
|
38
|
+
{ n: 1, label: "January" }, { n: 2, label: "February" }, { n: 3, label: "March" },
|
|
39
|
+
{ n: 4, label: "April" }, { n: 5, label: "May" }, { n: 6, label: "June" },
|
|
40
|
+
{ n: 7, label: "July" }, { n: 8, label: "August" }, { n: 9, label: "September" },
|
|
41
|
+
{ n: 10, label: "October" }, { n: 11, label: "November" }, { n: 12, label: "December" }
|
|
42
|
+
],
|
|
43
|
+
// Year range: 13 years ago (youngest plausible) back 100 years. This is a
|
|
44
|
+
// sane input range, not an age bar — the app's minAge decides eligibility.
|
|
45
|
+
years: (function () {
|
|
46
|
+
var ys = [], top = now.getFullYear() - 13, bottom = now.getFullYear() - 100;
|
|
47
|
+
for (var y = top; y >= bottom; y--) ys.push(y);
|
|
48
|
+
return ys;
|
|
49
|
+
})(),
|
|
50
|
+
// Days available for the chosen month/year (handles 28/29/30/31).
|
|
51
|
+
get dayOptions() {
|
|
52
|
+
var m = parseInt(this.month, 10), y = parseInt(this.year, 10);
|
|
53
|
+
var max = 31;
|
|
54
|
+
if (m) max = new Date(y || 2000, m, 0).getDate();
|
|
55
|
+
var ds = [];
|
|
56
|
+
for (var d = 1; d <= max; d++) ds.push(d);
|
|
57
|
+
return ds;
|
|
58
|
+
},
|
|
59
|
+
get complete() {
|
|
60
|
+
return !!(this.month && this.day && this.year);
|
|
61
|
+
},
|
|
62
|
+
get computedAge() {
|
|
63
|
+
if (!this.complete) return null;
|
|
64
|
+
var dob = new Date(parseInt(this.year, 10), parseInt(this.month, 10) - 1, parseInt(this.day, 10));
|
|
65
|
+
var age = now.getFullYear() - dob.getFullYear();
|
|
66
|
+
var hadBirthday = (now.getMonth() > dob.getMonth()) ||
|
|
67
|
+
(now.getMonth() === dob.getMonth() && now.getDate() >= dob.getDate());
|
|
68
|
+
if (!hadBirthday) age -= 1;
|
|
69
|
+
return age;
|
|
70
|
+
},
|
|
71
|
+
// Clamp an out-of-range day when the month changes (e.g. Feb after 31).
|
|
72
|
+
onMonthChange() {
|
|
73
|
+
if (this.day && parseInt(this.day, 10) > this.dayOptions.length) this.day = "";
|
|
74
|
+
this.error = "";
|
|
75
|
+
},
|
|
76
|
+
// Success path: flip a session flag if present, close via the app's store,
|
|
77
|
+
// and let the app resume its own flow (the entry gate listens for this).
|
|
78
|
+
_finish() {
|
|
79
|
+
try {
|
|
80
|
+
var sess = window.Alpine && Alpine.store && Alpine.store("session");
|
|
81
|
+
if (sess) sess.ageVerified = true;
|
|
82
|
+
} catch (e) {}
|
|
83
|
+
try { Alpine.store(this.store).close(); } catch (e) {}
|
|
84
|
+
window.dispatchEvent(new CustomEvent("age-verified"));
|
|
85
|
+
},
|
|
86
|
+
submit() {
|
|
87
|
+
if (!this.complete || this.submitting) return;
|
|
88
|
+
this.error = "";
|
|
89
|
+
// Client courtesy gate only — the server recompute is authoritative.
|
|
90
|
+
if (this.computedAge !== null && this.minAge && this.computedAge < this.minAge) return;
|
|
91
|
+
this.submitting = true;
|
|
92
|
+
var self = this;
|
|
93
|
+
if (this.demo) {
|
|
94
|
+
// Style-guide preview: no backend, resolve locally so the modal
|
|
95
|
+
// demonstrates the success handoff without an endpoint.
|
|
96
|
+
setTimeout(function () { self.submitting = false; self._finish(); }, 450);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || "";
|
|
100
|
+
fetch(this.url, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf, "Accept": "application/json" },
|
|
103
|
+
body: JSON.stringify({ year: this.year, month: this.month, day: this.day })
|
|
104
|
+
})
|
|
105
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
106
|
+
.then(function (res) {
|
|
107
|
+
self.submitting = false;
|
|
108
|
+
if (res.ok && res.body && res.body.verified) { self._finish(); return; }
|
|
109
|
+
self.error = (res.body && res.body.error) ||
|
|
110
|
+
"We couldn't verify your age. Please check the date and try again.";
|
|
111
|
+
})
|
|
112
|
+
.catch(function () {
|
|
113
|
+
self.submitting = false;
|
|
114
|
+
self.error = "Network error — please try again.";
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
})();
|
|
120
|
+
</script>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Age-gate DOB modal — engine primitive. Collects a real date of birth
|
|
3
|
+
(Month / Day / Year), shows a live "too young" hint, and submits. Homed in the
|
|
4
|
+
engine as the heavier sibling of studio/modals/shared/_age_attestation (the
|
|
5
|
+
lighter one-checkbox legal attestation). RUN ONE OR THE OTHER, not both: the
|
|
6
|
+
attestation is a signup-time "I confirm I'm of legal age" checkbox; this is an
|
|
7
|
+
entry-time DOB gate that a server can recompute and stamp.
|
|
8
|
+
|
|
9
|
+
CRITICAL — the engine hardcodes NO legal policy. It renders the modal UI, the
|
|
10
|
+
DOB fields, and the submit; everything legal is app-supplied:
|
|
11
|
+
|
|
12
|
+
min_age (Integer, REQUIRED) — the app's minimum age. There is NO engine
|
|
13
|
+
default (18 is itself a policy value); the app resolves it from
|
|
14
|
+
its own policy against its own server-detected jurisdiction.
|
|
15
|
+
submit_url (String, REQUIRED) — the app endpoint the DOB POSTs to. The
|
|
16
|
+
app owns the authoritative recompute + DOB persistence; the modal
|
|
17
|
+
only shows the response.
|
|
18
|
+
state (String, optional) — a PASSIVE jurisdiction label for the copy
|
|
19
|
+
(e.g. "CA"). Server-detected upstream; the modal never detects
|
|
20
|
+
geography and never offers an editable state field (a spoofable
|
|
21
|
+
client state must not be able to lower the bar). Blank => the
|
|
22
|
+
jurisdiction clause is dropped.
|
|
23
|
+
title (String, optional) — modal title. Default "Verify your age".
|
|
24
|
+
intro (String, optional) — the lead sentence after the age line.
|
|
25
|
+
fine_print (String, optional) — the app's legal / jurisdiction copy (e.g.
|
|
26
|
+
its per-state age table). The engine ships only a neutral,
|
|
27
|
+
policy-free default; the app passes its real copy here.
|
|
28
|
+
modal_store (String, optional) — Alpine store backing close(). Default "modals".
|
|
29
|
+
demo (Boolean, optional) — style-guide preview: resolve locally, no POST.
|
|
30
|
+
|
|
31
|
+
Requires window.ageVerifyModal — render studio/_age_verify_assets once at
|
|
32
|
+
layout level (the factory can't ship inside this template; a cloned script
|
|
33
|
+
never runs). Single root element (modal-host template mount rule).
|
|
34
|
+
%>
|
|
35
|
+
<%
|
|
36
|
+
min_age = local_assigns.fetch(:min_age)
|
|
37
|
+
submit_url = local_assigns.fetch(:submit_url)
|
|
38
|
+
state = local_assigns[:state].to_s.strip
|
|
39
|
+
title = local_assigns.fetch(:title, "Verify your age")
|
|
40
|
+
intro = local_assigns.fetch(:intro, "Enter your date of birth to confirm you're eligible — we only ask once.")
|
|
41
|
+
fine_print = local_assigns.fetch(:fine_print, "We use your date of birth only to confirm eligibility.")
|
|
42
|
+
modal_store = local_assigns.fetch(:modal_store, "modals")
|
|
43
|
+
demo = local_assigns.fetch(:demo, false)
|
|
44
|
+
%>
|
|
45
|
+
<div x-data="ageVerifyModal({ minAge: <%= min_age.to_i %>, state: '<%= j state %>', url: '<%= j submit_url %>', store: '<%= j modal_store %>', demo: <%= demo ? 'true' : 'false' %> })">
|
|
46
|
+
<%= render layout: "studio/modals/blocks/shell", locals: { title: title, modal_store: modal_store } do %>
|
|
47
|
+
<p class="text-sm text-muted mb-4">
|
|
48
|
+
You must be <span class="text-heading font-semibold"><%= min_age %>+</span><%= state.present? ? " in #{state}" : "" %>.
|
|
49
|
+
<%= intro %>
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<%# Month / Day / Year selectors %>
|
|
53
|
+
<div class="grid grid-cols-3 gap-2 mb-4">
|
|
54
|
+
<div>
|
|
55
|
+
<label class="label-upper block mb-1">Month</label>
|
|
56
|
+
<select x-model="month" @change="onMonthChange()" class="input-field !px-3 !py-2.5 text-sm appearance-none cursor-pointer">
|
|
57
|
+
<option value="" disabled>Month</option>
|
|
58
|
+
<template x-for="m in months" :key="m.n">
|
|
59
|
+
<option :value="m.n" x-text="m.label"></option>
|
|
60
|
+
</template>
|
|
61
|
+
</select>
|
|
62
|
+
</div>
|
|
63
|
+
<div>
|
|
64
|
+
<label class="label-upper block mb-1">Day</label>
|
|
65
|
+
<select x-model="day" @change="error = ''" class="input-field !px-3 !py-2.5 text-sm appearance-none cursor-pointer">
|
|
66
|
+
<option value="" disabled>Day</option>
|
|
67
|
+
<template x-for="d in dayOptions" :key="d">
|
|
68
|
+
<option :value="d" x-text="d"></option>
|
|
69
|
+
</template>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<label class="label-upper block mb-1">Year</label>
|
|
74
|
+
<select x-model="year" @change="onMonthChange()" class="input-field !px-3 !py-2.5 text-sm appearance-none cursor-pointer">
|
|
75
|
+
<option value="" disabled>Year</option>
|
|
76
|
+
<template x-for="y in years" :key="y">
|
|
77
|
+
<option :value="y" x-text="y"></option>
|
|
78
|
+
</template>
|
|
79
|
+
</select>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<%# Live too-young hint — shown once a full date is picked and it fails the
|
|
84
|
+
app-supplied bar, so the user sees the problem before submitting. %>
|
|
85
|
+
<template x-if="complete && computedAge !== null && minAge && computedAge < minAge">
|
|
86
|
+
<p class="text-red-400 text-sm mb-3" x-text="'You must be ' + minAge + '+' + (stateCode ? ' in ' + stateCode : '') + '.'"></p>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<%# Server error (invalid date, or the authoritative too-young check). %>
|
|
90
|
+
<template x-if="error">
|
|
91
|
+
<p class="text-red-400 text-sm mb-3" x-text="error"></p>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<button type="button" @click="submit()"
|
|
95
|
+
class="btn btn-primary btn-lg w-full"
|
|
96
|
+
:disabled="!complete || submitting || (computedAge !== null && minAge && computedAge < minAge)">
|
|
97
|
+
<span x-show="!submitting">Confirm & Continue</span>
|
|
98
|
+
<span x-show="submitting" class="inline-flex items-center justify-center gap-2" style="display: none;">
|
|
99
|
+
<span class="cta-spinner" aria-hidden="true"></span>
|
|
100
|
+
Verifying…
|
|
101
|
+
</span>
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
<p class="text-[11px] text-muted text-center mt-3 leading-snug"><%= fine_print %></p>
|
|
105
|
+
<% end %>
|
|
106
|
+
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%#
|
|
2
|
-
Wallet brand-icon sprite — inline
|
|
2
|
+
Wallet brand-icon sprite — inline brand marks for the wallets the Connect-Wallet
|
|
3
3
|
picker knows about (Phantom, Solflare, Backpack), OWNED BY THE ENGINE so every
|
|
4
4
|
consumer inherits crisp brand icons with ZERO per-app asset files. Before this,
|
|
5
5
|
the picker referenced app-served PNGs (/wallet-phantom.png, ...), so a fresh app
|
|
@@ -15,37 +15,26 @@
|
|
|
15
15
|
the name and returns it only for a known brand, else null → letter fallback).
|
|
16
16
|
|
|
17
17
|
The sprite is visually hidden (zero-box, absolute) and aria-hidden; only the
|
|
18
|
-
<use> references paint. Each <symbol> carries
|
|
19
|
-
a <
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
not in this set.
|
|
18
|
+
<use> references paint. Each <symbol> carries the OFFICIAL brand asset embedded
|
|
19
|
+
inline as a base64 PNG data-URI <image> — byte-identical to the real logos each
|
|
20
|
+
wallet ships (matching Turf Monster's public/wallet-*.png), so no hand-drawn
|
|
21
|
+
approximation drifts from the brand. The letter tile stays the fallback for any
|
|
22
|
+
wallet not in this set.
|
|
23
23
|
%>
|
|
24
24
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"
|
|
25
25
|
style="position:absolute;width:0;height:0;overflow:hidden" data-wallet-brand-sprite>
|
|
26
|
-
<%# Phantom — light-purple tile, cream ghost
|
|
27
|
-
|
|
28
|
-
<symbol id="se-wallet-phantom" viewBox="0 0 128 128">
|
|
29
|
-
<rect width="128" height="128" rx="28" fill="#AB9FF2"/>
|
|
30
|
-
<path fill="#FFFDF8" fill-rule="evenodd" d="M110.584 64.9142H99.142C99.142 41.7651 80.173 23 56.7724 23C33.6612 23 14.8716 41.3057 14.4118 64.0583C13.936 87.577 36.2456 108 60.0197 108H63.0263C83.9754 108 112.069 91.6259 116.459 71.7107C117.27 68.033 114.351 64.9142 110.584 64.9142ZM39.7689 65.9482C39.7689 69.0453 37.2384 71.5758 34.1413 71.5758C31.0442 71.5758 28.5137 69.0453 28.5137 65.9482V56.8554C28.5137 53.7583 31.0442 51.2278 34.1413 51.2278C37.2384 51.2278 39.7689 53.7583 39.7689 56.8554V65.9482ZM59.3312 65.9482C59.3312 69.0453 56.8007 71.5758 53.7036 71.5758C50.6065 71.5758 48.076 69.0453 48.076 65.9482V56.8554C48.076 53.7583 50.6065 51.2278 53.7036 51.2278C56.8007 51.2278 59.3312 53.7583 59.3312 56.8554V65.9482Z"/>
|
|
26
|
+
<%# Phantom — official brand mark (light-purple tile, cream ghost). %>
|
|
27
|
+
<symbol id="se-wallet-phantom" viewBox="0 0 460 460">
|
|
28
|
+
<image width="460" height="460" href="data:image/png;base64,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"/>
|
|
31
29
|
</symbol>
|
|
32
30
|
|
|
33
|
-
<%# Solflare —
|
|
34
|
-
<symbol id="se-wallet-solflare" viewBox="0 0
|
|
35
|
-
<rect width="100" height="100" rx="24" fill="#FFEC1F"/>
|
|
36
|
-
<path d="M67 37c-3-7-11-11-20-11-10 0-17 5-17 13 0 7 5 11 16 14l8 2c9 2 13 5 13 11 0 8-8 13-18 13-9 0-16-3-19-10"
|
|
37
|
-
fill="none" stroke="#131313" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
38
|
-
<path d="M27 76 73 24" fill="none" stroke="#131313" stroke-width="8" stroke-linecap="round"/>
|
|
31
|
+
<%# Solflare — official brand mark (yellow tile, black blackletter S). %>
|
|
32
|
+
<symbol id="se-wallet-solflare" viewBox="0 0 361 361">
|
|
33
|
+
<image width="361" height="361" href="data:image/png;base64,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"/>
|
|
39
34
|
</symbol>
|
|
40
35
|
|
|
41
|
-
<%# Backpack — red tile, white pack
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<rect width="100" height="100" rx="24" fill="#E33E3F"/>
|
|
45
|
-
<path d="M39 32v-3a11 11 0 0 1 22 0v3" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round"/>
|
|
46
|
-
<rect x="23" y="31" width="54" height="31" rx="15" fill="#fff"/>
|
|
47
|
-
<circle cx="50" cy="46" r="8" fill="#E33E3F"/>
|
|
48
|
-
<circle cx="50" cy="46" r="3.5" fill="#fff"/>
|
|
49
|
-
<rect x="23" y="66" width="54" height="13" rx="6.5" fill="#fff"/>
|
|
36
|
+
<%# Backpack — official brand mark (red tile, white pack). %>
|
|
37
|
+
<symbol id="se-wallet-backpack" viewBox="0 0 128 128">
|
|
38
|
+
<image width="128" height="128" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAYCSURBVHgB7d1vThtHGAbw513bUr/VXysFQk5QOEDt5QQlJwg5AXAC2ycgOUHJCQInYAkHiHuCuCFSv/pjJex5O2Obqg2Q2mFnZnfn+UkoClGw8Tw7f3ZmZwAiIiIiIiIiIkqCIDGf8rz7A9CdATtf/5v9MKa3wORFUUyRiMYH4CbPcxjTh0gOxa79VneN/za1n8wYKlfIUGwVRYGGamQA3FXeNuZIRA5V71/pm7If0sSojDotFD8VxQQN0rgAfM7zAzH6G9a70jdyF4Tt6+IMDdGoAHzu9U4FcgzPFPpmnmWjJvQVGhGARZWverlq48OwfYSZyH7dQ5ChAVrGDIIWvmNfz4buPWqu9gG46fUGIar9BynyP37J47x2SWrdBNiqf6dt9CM8dPg2MG1nslfX0UEbNdZRDNQWvh3uTexfCxj8blqY2mpt3LIF85f9KqON/vfNo8XrGdvcCH6Gu5mkujvTxahjHxSWHfIdusJBRO713dATRERERER10bjJINcr76yGa4pyZgLdn3a8PGnaTKBT+wC4+X41+FVEdzeY7/9+gkJVxvYe6pUN2rjuoahlAP4pdOgh4t4FXAbCyLt5C+d1nBiqVQAWq3tU3cRPjopZNBU2DC2RUZ1qhVoE4Eue76rqaRUL/mE6nGXZ2zrUCJUPgJ3tO7Jv8w1qZrF6KJOT7aI4R4VVNgCrRR7v63PVP0aHWx8+jFBRlQyAm+btqF6WsaCzEgTjtsjLKvYNKheAxhX+imsSWpnsVy0ElQpAyMJ3awjUrf9ffsHN68PzkLKKIahUAG76/Y9e1/aJFGJw0bJj9ocKwY02MLd9jkyPfIXQheA2k72qjBAqEwC3ts++nSH8mIrK6Nl1sfZo4qaXD221MIAPgvOtq6uXqIBKBGD5+JZewoOnVLs+mySjcvJ8g0D6UolVwbJcU1f+z31im2ur6cmtyL7IckKoTJno6aLJiSx6AFzV76m9nZbR4XIhUJHX8EA9BX8TUQPgqlj3ACc8cG1+Wb3t5dPB8hZlsx3e2M8VRA1A25hXPq5+V/U/K7l9nWUY4m7IWCLbFAxirmyOGgBfV79CLlAyN2zz8XOtbtsgWi0QLQBuTb+3Gz4Z/EzAZCjghR4hkmgBsD3/V/DE144ec/gKALqLoXAEUQLgOn++Zvnu1vB54u/unXq66fQ/ogSghXpO8Xq9fWsviBidwSgByBR9eKIeJ3R8F1BnjuDPGEbqA2gOf7q+CqoNz5tQZOrtwnj0JRGYKxzf072+mhiZew5AhNVPwQPg/SqCvxGGir+Ry+LnAzuh+wHBA+D9KnIUB3+6kUaJVsM07+89dAc5eABUwiz1mrll5CWSQBM32TzsUrjwnUDBc4Rga4GyJlo8zljeE+oCuRNhFKDB2rjFnHuv96R22/NKpXtE9EcEFD4AGvZZPjuBc7YsxM25nUdDFr5j3+8OAgq+S5idAbTDQEVYMrzp9w/si5+sM0/gOnyuzW/a0vSH1HqbuI241caql196/YmBnEuGC3c+gNtOzk7ydI3bS8Cgv3ji2Gg3dERjSScAK26sbQv52Bb2sSvk2er7jdspY02N2CuYvh8DkDgGIHEMQOIYgMQxAIljABLHACSOAUgcA5A4BiBxDEDiGIDEMQCJYwASxwAkjgFIXPjnAlRrd6hCk4V/MkjlHehRahD08wkegOXmTTLysfdezbndTE+2r4szEBEREREREfkR7ZG45SERdT8SrjyaYRzjjMHgAXC7hK7OA4x+WELlCMYzkf2Q5wkFvxPYUQxY+I+wn0voncNjTAax8L9F/O2i+pDwk0EicY97p//geoDEMQCJYwASxwAkjgFIHAOQOAYgcQxA4hiAxDEAiWMAEscAJI4BSBwDkDgGIHEMQOIYgMQxAIljABIXflEoMAY9SqATBBTj6FjuEPINKtLsHUIWT78ICtA9Ijhb51zDMkXpA8xEXnOLmK8IxrciJwgsSgBeFMXE/rJ7Cm4YZU3dnkmhHwm7E/28xE953m0Du+5ARyTGfviTme0Uxyh4IiIiIiIiIiIiIiJKw9/zcyBkOm/y4QAAAABJRU5ErkJggg=="/>
|
|
50
39
|
</symbol>
|
|
51
40
|
</svg>
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
card behind ours. So this section owns a tiny, self-contained store + overlay
|
|
15
15
|
and registers all specimen content itself.
|
|
16
16
|
|
|
17
|
-
Groups: Auth (the sign-in step machine),
|
|
17
|
+
Groups: Auth (the sign-in step machine), Eligibility and entry (the
|
|
18
|
+
app-supplied-policy age-gate DOB primitive, gated by Studio.feature?(:age_gate),
|
|
19
|
+
plus the app-specific entry-tokens purchase flow documented via engine chrome),
|
|
20
|
+
Profile (crop / upload, opened
|
|
18
21
|
through studio/cropper_assets), Web3 (wallet-connect with engine-owned inline
|
|
19
22
|
brand icons / on-chain-tx / deposit / the generic entry-confirmed celebration,
|
|
20
23
|
gated by Studio.feature?(:web3) — disabled-but-present-yet-openable when off),
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
<%
|
|
29
32
|
web3_on = Studio.feature?(:web3)
|
|
30
33
|
leveling_on = Studio.feature?(:leveling)
|
|
34
|
+
age_gate_on = Studio.feature?(:age_gate)
|
|
31
35
|
|
|
32
36
|
# Auth method-toggle defaults — mirror the app's Studio.auth_method? config;
|
|
33
37
|
# Solana Wallet also needs the web3 capability, so it defaults OFF on an app
|
|
@@ -51,8 +55,17 @@
|
|
|
51
55
|
expr
|
|
52
56
|
end
|
|
53
57
|
%>
|
|
58
|
+
<%# DEMO flow wiring (style-page only): the age-gate primitive fires an
|
|
59
|
+
'age-verified' window event on a successful confirm — its designed handoff
|
|
60
|
+
hook, the same one a production app listens for to resume the entry. Here the
|
|
61
|
+
style page plays that "app": on age-verified it opens the entry-token purchase
|
|
62
|
+
modal, so the DS walks eligibility to purchase in sequence. The engine
|
|
63
|
+
primitive is untouched; its real submit still posts to the app-supplied
|
|
64
|
+
submit_url. Kept on the section (always mounted) so it fires even as the
|
|
65
|
+
age-gate card closes. %>
|
|
54
66
|
<section id="modals" class="space-y-10" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
|
|
55
|
-
x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
|
|
67
|
+
x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
|
|
68
|
+
@age-verified.window="$store.dsModals.open('entry-tokens', { step: 'picker' })">
|
|
56
69
|
<div class="space-y-1">
|
|
57
70
|
<h2 class="text-2xl font-bold text-heading">Modals</h2>
|
|
58
71
|
<p class="text-muted text-sm">
|
|
@@ -237,6 +250,11 @@
|
|
|
237
250
|
defines the factory inline; no importmap change). %>
|
|
238
251
|
<%= render "studio/cropper_assets" %>
|
|
239
252
|
|
|
253
|
+
<%# ageVerifyModal factory — rendered here at page level (never inside the
|
|
254
|
+
overlay template, where a cloned script would not run) so the age-gate
|
|
255
|
+
specimen opens live. A consumer app renders this in its layout the same way. %>
|
|
256
|
+
<%= render "studio/age_verify_assets" %>
|
|
257
|
+
|
|
240
258
|
<%# ---- The overlay: one backdrop + card; every specimen's real content is a
|
|
241
259
|
single-root <template x-if> gated on the current modal id. Faithful to the
|
|
242
260
|
shared host — backdrop fade, scroll lock, escape + click-self dismissal
|
|
@@ -257,6 +275,14 @@
|
|
|
257
275
|
<div><%= render "style/modals/auth" %></div>
|
|
258
276
|
</template>
|
|
259
277
|
|
|
278
|
+
<%# --- Eligibility + entry --- %>
|
|
279
|
+
<template x-if="$store.dsModals.current().id === 'age-verify'">
|
|
280
|
+
<div><%= render "style/modals/age_verify" %></div>
|
|
281
|
+
</template>
|
|
282
|
+
<template x-if="$store.dsModals.current().id === 'entry-tokens'">
|
|
283
|
+
<div><%= render "style/modals/entry_tokens" %></div>
|
|
284
|
+
</template>
|
|
285
|
+
|
|
260
286
|
<%# --- Profile (real engine partials, opened on dsModals) --- %>
|
|
261
287
|
<template x-if="$store.dsModals.current().id === 'crop-photo'">
|
|
262
288
|
<div><%= render "studio/modals/crop_photo", store: "dsModals" %></div>
|
|
@@ -432,6 +458,68 @@
|
|
|
432
458
|
</div>
|
|
433
459
|
</section>
|
|
434
460
|
|
|
461
|
+
<%# ===================================================================== %>
|
|
462
|
+
<%# 1b. ELIGIBILITY + ENTRY %>
|
|
463
|
+
<%# ===================================================================== %>
|
|
464
|
+
<section class="space-y-5">
|
|
465
|
+
<div class="space-y-1">
|
|
466
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
467
|
+
<h3 class="text-xl font-bold text-heading">Eligibility & entry</h3>
|
|
468
|
+
<% if age_gate_on %>
|
|
469
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
|
|
470
|
+
<% else %>
|
|
471
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
|
|
472
|
+
<% end %>
|
|
473
|
+
</div>
|
|
474
|
+
<p class="text-muted text-sm">
|
|
475
|
+
The pre-entry gate and the entry-purchase flow. The <strong>age gate</strong>
|
|
476
|
+
(<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is a
|
|
477
|
+
NEW engine primitive gated by
|
|
478
|
+
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> — a DOB
|
|
479
|
+
modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
480
|
+
<strong>app-supplied</strong> (the engine hardcodes no policy). It is the heavier
|
|
481
|
+
sibling of the <code class="font-mono text-2xs">shared/_age_attestation</code>
|
|
482
|
+
checkbox inside Sign in above — <strong>run one or the other</strong>. The
|
|
483
|
+
<strong>entry-tokens</strong> flow is <strong>app-specific</strong> (payments
|
|
484
|
+
product): documented here composing engine chrome, but the packs, rails, and
|
|
485
|
+
the on-chain mint stay app-owned.
|
|
486
|
+
</p>
|
|
487
|
+
</div>
|
|
488
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
489
|
+
<%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
|
|
490
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
491
|
+
label: "Age gate (DOB)",
|
|
492
|
+
reference: %(the "Age gate" DOB modal (studio-engine studio/modals/blocks/_age_verify) — a NEW engine primitive; the app supplies min_age / state / submit_url / fine_print and the engine hardcodes no legal policy. In this demo, Confirm & Continue advances to the Entry tokens purchase (page-level dsModals wiring on the primitive's 'age-verified' hook; the primitive's real submit posts to submit_url). Open with $store.dsModals.open('age-verify')),
|
|
493
|
+
open_expr: "$store.dsModals.open('age-verify')",
|
|
494
|
+
glow_when: ds_glow.call("age-verify"),
|
|
495
|
+
disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
|
|
496
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
497
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
498
|
+
<div class="grid grid-cols-3 gap-1">
|
|
499
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
500
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
501
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
502
|
+
</div>
|
|
503
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
504
|
+
</div>
|
|
505
|
+
<% end %>
|
|
506
|
+
|
|
507
|
+
<%# Entry tokens — app-specific flow, documented (not capability-gated). %>
|
|
508
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
509
|
+
label: "Entry tokens",
|
|
510
|
+
reference: %(the "Entry tokens" purchase flow (app-specific — documented in the engine style guide composing studio/modals/blocks chrome). The packs, pricing, rails, and on-chain mint stay app-owned. Open with $store.dsModals.open('entry-tokens', { step: 'picker' })),
|
|
511
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
512
|
+
glow_when: ds_glow.call("entry-tokens") } do %>
|
|
513
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
514
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
515
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
516
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
517
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
518
|
+
</div>
|
|
519
|
+
<% end %>
|
|
520
|
+
</div>
|
|
521
|
+
</section>
|
|
522
|
+
|
|
435
523
|
<%# ===================================================================== %>
|
|
436
524
|
<%# 2. PROFILE — crop / upload, live via cropper_assets %>
|
|
437
525
|
<%# ===================================================================== %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen for the engine age-gate DOB modal
|
|
3
|
+
(studio/modals/blocks/_age_verify). Opened on the page-scoped dsModals host.
|
|
4
|
+
|
|
5
|
+
The whole point of this specimen is the SEAM: every legal value here is a DEMO
|
|
6
|
+
the app feeds — the engine primitive hardcodes none of it. So we pass an
|
|
7
|
+
illustrative policy (21+ in CA) and a demo fine_print that SAYS so, and run in
|
|
8
|
+
demo mode (no backend — submit resolves locally). A real app swaps these for
|
|
9
|
+
its own min_age / state / submit_url / legal copy resolved server-side.
|
|
10
|
+
%>
|
|
11
|
+
<%= render "studio/modals/blocks/age_verify",
|
|
12
|
+
min_age: 21,
|
|
13
|
+
state: "CA",
|
|
14
|
+
submit_url: "#demo-age-verify",
|
|
15
|
+
modal_store: "dsModals",
|
|
16
|
+
demo: true,
|
|
17
|
+
fine_print: "Demo policy the app supplies: 21+ in CA. The engine hardcodes no age and no state table — your app passes min_age, the jurisdiction label, this copy, and the endpoint that recomputes it server-side." %>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the entry-token purchase flow. This documents an
|
|
3
|
+
APP-SPECIFIC flow (Turf Monster's "Get Entry Tokens"); the engine deliberately
|
|
4
|
+
does NOT own the packs, pricing, or payment rails. The specimen composes only
|
|
5
|
+
engine CHROME (studio/modals/blocks/shell, card_header, progress_pill) and
|
|
6
|
+
represents the packs as an illustrative demo — the app owns the payment logic
|
|
7
|
+
and the on-chain mint.
|
|
8
|
+
|
|
9
|
+
Opened on the page-scoped dsModals host with an internal step machine on
|
|
10
|
+
props.step: picker -> confirming -> minted. Single-root templates throughout.
|
|
11
|
+
|
|
12
|
+
On-chain model (documented, not owned here): an entry token is a PROGRAM-OWNED
|
|
13
|
+
prepaid entry credit recorded on-chain (a per-token account), NOT a fungible
|
|
14
|
+
SPL token held in the user's wallet. Two distinct on-chain moments: (1) MINT at
|
|
15
|
+
purchase, server-signed after the payment clears — not on the buyer's click;
|
|
16
|
+
(2) CONSUME at entry, when the app's hold-to-confirm gesture spends one token to
|
|
17
|
+
create the contest entry.
|
|
18
|
+
%>
|
|
19
|
+
<%
|
|
20
|
+
# Illustrative packs only — a real app reads these from its own config
|
|
21
|
+
# (e.g. StripePurchase.available_packs), and they vary by funding mode.
|
|
22
|
+
demo_packs = [
|
|
23
|
+
{ tokens: 1, price: "$5", note: nil },
|
|
24
|
+
{ tokens: 3, price: "$12", note: "Popular" },
|
|
25
|
+
{ tokens: 10, price: "$35", note: "Best value" }
|
|
26
|
+
]
|
|
27
|
+
%>
|
|
28
|
+
<div x-data="{
|
|
29
|
+
get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
|
|
30
|
+
buy(count, balance) {
|
|
31
|
+
Alpine.store('dsModals').advance({ step: 'confirming', mintedCount: count, mintedBalance: balance });
|
|
32
|
+
setTimeout(function () {
|
|
33
|
+
var c = Alpine.store('dsModals').current();
|
|
34
|
+
if (c && c.id === 'entry-tokens') Alpine.store('dsModals').advance({ step: 'minted' });
|
|
35
|
+
}, 1300);
|
|
36
|
+
}
|
|
37
|
+
}" class="relative">
|
|
38
|
+
|
|
39
|
+
<%# Shared close — top-right, above whichever step is active. %>
|
|
40
|
+
<button @click="$store.dsModals.close()"
|
|
41
|
+
class="absolute top-0 right-0 -mt-2 -mr-2 text-secondary hover:text-heading text-xl leading-none z-10"
|
|
42
|
+
aria-label="Close">×</button>
|
|
43
|
+
|
|
44
|
+
<%# ===== PICKER ===== %>
|
|
45
|
+
<template x-if="(props.step || 'picker') === 'picker'">
|
|
46
|
+
<div>
|
|
47
|
+
<div class="text-center pt-1 mb-3">
|
|
48
|
+
<h3 class="text-heading font-bold text-lg leading-tight">Get Entry Tokens</h3>
|
|
49
|
+
</div>
|
|
50
|
+
<%= render "studio/modals/blocks/progress_pill", current: 2, total: 3 %>
|
|
51
|
+
<p class="text-xs text-secondary mb-4 text-center">
|
|
52
|
+
1 token = 1 contest entry. Pick a pack to finish.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<div class="space-y-2.5">
|
|
56
|
+
<% demo_packs.each do |pack| %>
|
|
57
|
+
<button type="button"
|
|
58
|
+
@click="buy(<%= pack[:tokens] %>, '<%= pack[:tokens] %>')"
|
|
59
|
+
class="w-full flex items-center justify-between gap-3 rounded-lg border border-strong bg-inset px-4 py-3 text-left hover:border-primary transition">
|
|
60
|
+
<span class="flex items-center gap-2">
|
|
61
|
+
<span class="text-heading font-semibold"><%= pack[:tokens] %> token<%= "s" unless pack[:tokens] == 1 %></span>
|
|
62
|
+
<% if pack[:note] %>
|
|
63
|
+
<span class="badge" style="color: var(--color-primary); border-color: var(--color-primary)"><%= pack[:note] %></span>
|
|
64
|
+
<% end %>
|
|
65
|
+
</span>
|
|
66
|
+
<span class="text-heading font-mono font-bold"><%= pack[:price] %></span>
|
|
67
|
+
</button>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<%# Rails — token-funded only. The picker documents which rails mint tokens.
|
|
72
|
+
The USDC-direct path is intentionally NOT a pack here (it's a separate
|
|
73
|
+
rail that transfers USDC and mints no token). %>
|
|
74
|
+
<div class="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted">
|
|
75
|
+
<span>Pay by card</span>
|
|
76
|
+
<span aria-hidden="true">·</span>
|
|
77
|
+
<span>PayPal / Venmo</span>
|
|
78
|
+
</div>
|
|
79
|
+
<p class="text-center text-2xs text-muted mt-1">USDC-direct entry is a separate rail — it funds an entry with no token.</p>
|
|
80
|
+
|
|
81
|
+
<%# The seam callout — what the engine owns vs what the app owns. %>
|
|
82
|
+
<div class="mt-4 rounded-lg border border-subtle bg-surface p-3 text-2xs text-secondary leading-snug">
|
|
83
|
+
<p class="text-heading font-semibold mb-1">App-specific flow, engine chrome</p>
|
|
84
|
+
<p class="mb-1">
|
|
85
|
+
The engine supplies only the shell, header, and progress pill. The packs,
|
|
86
|
+
pricing, payment rails, and the on-chain mint are <strong>app-owned</strong> —
|
|
87
|
+
this specimen represents them as a demo.
|
|
88
|
+
</p>
|
|
89
|
+
<p>
|
|
90
|
+
On-chain, an entry token is a <strong>prepaid entry credit recorded on-chain</strong>
|
|
91
|
+
(not a transferable token held in your wallet): minted server-side once payment
|
|
92
|
+
clears, then <strong>consumed</strong> when the app's hold-to-confirm creates the entry.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<%# ===== CONFIRMING (mint-at-purchase) ===== %>
|
|
99
|
+
<template x-if="props.step === 'confirming'">
|
|
100
|
+
<div x-init="setTimeout(function () { var c = Alpine.store('dsModals').current(); if (c && c.id === 'entry-tokens') Alpine.store('dsModals').advance({ step: 'minted' }); }, 1300)">
|
|
101
|
+
<%= render "studio/modals/blocks/card_header",
|
|
102
|
+
spinner: true,
|
|
103
|
+
title: "Confirming your purchase…",
|
|
104
|
+
subtitle: "Payment cleared — a server job mints one prepaid entry credit per token on-chain." %>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<%# ===== MINTED (ready to consume at entry) ===== %>
|
|
109
|
+
<template x-if="props.step === 'minted'">
|
|
110
|
+
<div>
|
|
111
|
+
<%= render "studio/modals/blocks/card_header",
|
|
112
|
+
icon_color: 'primary',
|
|
113
|
+
title_key: "(props.mintedCount === 1 ? 'Entry Token Minted' : 'Entry Tokens Minted')" do %>
|
|
114
|
+
You now have <span class="font-bold text-primary font-mono" x-text="props.mintedBalance"></span> available.
|
|
115
|
+
<% end %>
|
|
116
|
+
<p class="text-xs text-secondary text-center mb-4 leading-snug">
|
|
117
|
+
In the app, holding to confirm now <strong>consumes one token on-chain</strong> to
|
|
118
|
+
create your contest entry, then resolves to the shared Entry Confirmed card.
|
|
119
|
+
</p>
|
|
120
|
+
<button @click="$store.dsModals.close()" class="btn btn-primary btn-lg w-full">Done</button>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
</div>
|
data/lib/studio/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -243,6 +243,7 @@ files:
|
|
|
243
243
|
- app/views/schema/index.html.erb
|
|
244
244
|
- app/views/sessions/_sso_continue.html.erb
|
|
245
245
|
- app/views/sessions/new.html.erb
|
|
246
|
+
- app/views/studio/_age_verify_assets.html.erb
|
|
246
247
|
- app/views/studio/_confirm_interstitial.html.erb
|
|
247
248
|
- app/views/studio/_cropper_assets.html.erb
|
|
248
249
|
- app/views/studio/admin_models/_arenas_table.html.erb
|
|
@@ -263,6 +264,7 @@ files:
|
|
|
263
264
|
- app/views/studio/modals/_image_upload.html.erb
|
|
264
265
|
- app/views/studio/modals/_saving.html.erb
|
|
265
266
|
- app/views/studio/modals/auth/_resend_footer.html.erb
|
|
267
|
+
- app/views/studio/modals/blocks/_age_verify.html.erb
|
|
266
268
|
- app/views/studio/modals/blocks/_card_header.html.erb
|
|
267
269
|
- app/views/studio/modals/blocks/_cta_redirect.html.erb
|
|
268
270
|
- app/views/studio/modals/blocks/_digit_reel.html.erb
|
|
@@ -293,7 +295,9 @@ files:
|
|
|
293
295
|
- app/views/style/_theme.html.erb
|
|
294
296
|
- app/views/style/_tricks.html.erb
|
|
295
297
|
- app/views/style/index.html.erb
|
|
298
|
+
- app/views/style/modals/_age_verify.html.erb
|
|
296
299
|
- app/views/style/modals/_auth.html.erb
|
|
300
|
+
- app/views/style/modals/_entry_tokens.html.erb
|
|
297
301
|
- app/views/style/modals/_onchain_tx.html.erb
|
|
298
302
|
- app/views/style/modals/_wallet_connect.html.erb
|
|
299
303
|
- app/views/style/modals/_wallet_deposit.html.erb
|