studio-engine 0.74.1 → 0.74.2
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 +94 -7
- data/app/views/studio/mailers/_layered_banner.html.erb +37 -2
- data/app/views/style/_modals.html.erb +4 -127
- data/lib/studio/js_literal.rb +20 -0
- data/lib/studio/version.rb +1 -1
- metadata +1 -6
- data/app/views/style/modals/_cosign_rejected.html.erb +0 -36
- data/app/views/style/modals/_ds_buy_entry_token.html.erb +0 -40
- data/app/views/style/modals/_ds_cdp_ramp.html.erb +0 -41
- data/app/views/style/modals/_wallet_changed.html.erb +0 -70
- data/app/views/style/modals/_wallet_setup.html.erb +0 -112
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6694226dcc9e100149692ef60162478d4927a69280343c5047c0222517f1f68c
|
|
4
|
+
data.tar.gz: 8e6b5c07f8bb5fa60466049f321c7be0a09168fb750766202a5d122734cb6699
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ec39596a087d56cc64f3a211ea349bf6a00b7a21c6eb1d1c70b36136dda3f3ab7f8fe1a1f61b26593661283f12707501727d9f3d5119f01b3fa28995ece7bd7
|
|
7
|
+
data.tar.gz: 3d65093bf62c9088976d9ea56e756cabd7e01e65aaae06205419e337a0d8b6a1a1b93c60b93631bf724d254f01bcdc5fb0c7360c50525c1f519b35b6b7b49946
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,84 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
6
6
|
|
|
7
7
|
### Fixed
|
|
8
8
|
|
|
9
|
+
- **The email banner assembled two attributes INCLUDING THEIR OWN QUOTES in Ruby,
|
|
10
|
+
so ERB escaping never ran on them.** `_layered_banner.html.erb` wrote
|
|
11
|
+
`background="..."` and `bgcolor="..."` as Ruby strings and marked them
|
|
12
|
+
`html_safe` — the one shape ERB's attribute escaping can never reach. Both now
|
|
13
|
+
go through `tag.attributes`. This is the EMAIL half of the family PR #303 closed
|
|
14
|
+
in the modal/Alpine partials.
|
|
15
|
+
|
|
16
|
+
**THE RULE IS THE OPPOSITE OF #303'S, and that is the whole point.** There the
|
|
17
|
+
locals are Alpine EXPRESSIONS: they stay `html_safe` so a handler reaches the
|
|
18
|
+
browser byte-identical. A background URL and a colour are CONTENT, so they take
|
|
19
|
+
the FULL escaping a content attribute is owed — the treatment #303 gave the four
|
|
20
|
+
input constraints. Mark by what the value IS, not by which file it lives in.
|
|
21
|
+
|
|
22
|
+
**MEASURED, and worse than the sibling seams.** A double quote in
|
|
23
|
+
`background_url` did not merely inject an attribute, it ATE the rest of the tag:
|
|
24
|
+
the cell came back carrying `background`, `quote"`, a lone backslash, `<` and
|
|
25
|
+
`script`, having LOST `bgcolor`, `width`, `height`, `valign` and `style`
|
|
26
|
+
outright — five attributes gone from one quote. The failure is
|
|
27
|
+
a structurally wrecked email — no fallback colour, no dimensions, no
|
|
28
|
+
background-image CSS — not merely a tainted one.
|
|
29
|
+
|
|
30
|
+
**`bgcolor` WAS CLASSIFIED AND CLEARED BEFORE BEING TOUCHED**, because #303's
|
|
31
|
+
warning was that two of its six candidate sites were already safe.
|
|
32
|
+
`scrim_solid_hex` formats three integers the model has already clamped to 0-255,
|
|
33
|
+
so it returns a hash and six hex digits for every input — proven by rendering a
|
|
34
|
+
hostile scrim, not by reading the method. It is repaired anyway, and the reason
|
|
35
|
+
is measured: simulating the plausible refactor (the method learning to pass a
|
|
36
|
+
named colour through) injects an `onload` into every email with the attribute
|
|
37
|
+
hand-assembled, and merely renders a wrong colour with `tag.attributes` on it.
|
|
38
|
+
|
|
39
|
+
**NOTHING MOVED FOR REAL INPUT.** Seven banner shapes rendered before and after,
|
|
40
|
+
in both email and preview mode, are byte-identical. The one input whose bytes
|
|
41
|
+
change is a signed CDN URL, whose `&` now ships as `&` — a correction, since
|
|
42
|
+
the CSS twin and the VML `src` on the SAME element already escaped it and the
|
|
43
|
+
hand-assembled `background` was the odd one out shipping a bare `&`. Verified in
|
|
44
|
+
a real browser: the client decodes the attribute, requests the URL with its
|
|
45
|
+
signature intact, and paints the banner.
|
|
46
|
+
|
|
47
|
+
- **The Rails guard sweep's non-vacuity check tested a COPY of the scanner, so
|
|
48
|
+
the scanner could go blind and the suite stayed green.**
|
|
49
|
+
`rails_guard_sweep_test.rb` is a source scan, and a scan is only trustworthy
|
|
50
|
+
with a control proving its predicate can still say NO. That control re-typed
|
|
51
|
+
the rule as string operations on a sample instead of calling the scan's own
|
|
52
|
+
path: the predicate existed twice, at line 78 and line 101. Measured — typing
|
|
53
|
+
one typo into line 78 ALONE (`\bRails\.` → `\bRailz\.`), which blinds the net so
|
|
54
|
+
it can never flag anything no matter what `lib/` contains, left all three tests
|
|
55
|
+
passing, 0 failures. The floor was being measured against a duplicate
|
|
56
|
+
implementation, so it certified the copy and never the instrument.
|
|
57
|
+
|
|
58
|
+
**THE REPAIR IS ONE DEFINITION, NOT A BETTER COPY.** The rule now lives in a
|
|
59
|
+
single `offending_line?`, and both the scan and every control call it. The same
|
|
60
|
+
mutation now reddens `test_the_predicate_still_flags_the_shape_that_was_fixed`
|
|
61
|
+
by name — while the scan itself still passes, which is precisely the "passes for
|
|
62
|
+
free" the control exists to catch.
|
|
63
|
+
|
|
64
|
+
**THE CONTROLS ARE SPLIT ONE PER CLAUSE**, because `offending_line?` decides on
|
|
65
|
+
four (comment exemption, mentions `defined?(Rails)`, not already asking
|
|
66
|
+
`Rails.respond_to?`, and calls a method on the constant) and one combined
|
|
67
|
+
assertion would go red without saying which clause went inert — the same
|
|
68
|
+
blindness as the copy, one level in. Each of the four now reddens its own named
|
|
69
|
+
test. The scan also asserts its EXIT-BLINDNESS FLOOR inside the test that trusts
|
|
70
|
+
the result — the three named files were swept, and more than 1,000 lines were
|
|
71
|
+
read — because a scan whose loop never runs reports zero offenders and passes
|
|
72
|
+
having proved nothing. Six mutations, applied one at a time, each verified red.
|
|
73
|
+
|
|
74
|
+
**TWO CLAIMS IN THE ENTRIES BELOW WERE FALSE, and are corrected in this same
|
|
75
|
+
change.** *The Rails guard sweep really is finished now* said a bare
|
|
76
|
+
`require "action_view"` is what defines the namespace-only `module Rails`. It is
|
|
77
|
+
not — measured, that require leaves `Rails` UNDEFINED and never loads
|
|
78
|
+
rails-html-sanitizer. What defines it is naming a constant under
|
|
79
|
+
`ActionView::Helpers`, which autoloads the helpers tree; in `studio/js_literal`
|
|
80
|
+
that is line 66's `extend ActionView::Helpers::JavaScriptHelper`. The same loose
|
|
81
|
+
sentence had propagated into three test files and is corrected there too. And
|
|
82
|
+
*`Studio::S3` no longer mistakes a Rails NAMESPACE* said `lib/studio.rb` used the
|
|
83
|
+
repaired form "in three places" — the sweep in this same unreleased block made it
|
|
84
|
+
FOUR, so a false completeness claim was shipping one sentence above the entry
|
|
85
|
+
announcing a fix for exactly that.
|
|
86
|
+
|
|
9
87
|
- **A quote in a host local can no longer close an Alpine attribute and turn the
|
|
10
88
|
rest of the element into markup.** Four partials built an attribute — its OWN
|
|
11
89
|
QUOTES included — as a Ruby String and marked it `html_safe`, which is the one
|
|
@@ -81,9 +159,16 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
81
159
|
`Studio.routes`. All three now ask `Rails.respond_to?` first.
|
|
82
160
|
|
|
83
161
|
**THE MAIL TRANSPORT ONE WAS ARMED, not theoretical.** `lib/studio.rb` requires
|
|
84
|
-
`studio/js_literal` (line 11)
|
|
85
|
-
|
|
86
|
-
`
|
|
162
|
+
`studio/js_literal` (line 11) BEFORE it requires `studio/mail_transport`
|
|
163
|
+
(line 25), and loading `js_literal` is what puts the namespace-only `module
|
|
164
|
+
Rails` in memory. **Not its `require "action_view"`** — measured, that require
|
|
165
|
+
alone leaves `Rails` UNDEFINED and never loads the sanitizer at all. It is line
|
|
166
|
+
66, `extend ActionView::Helpers::JavaScriptHelper`: naming anything under
|
|
167
|
+
`ActionView::Helpers` autoloads the helpers tree, which requires
|
|
168
|
+
rails-html-sanitizer, and that gem is what ships `module Rails` — with
|
|
169
|
+
`Rails.methods(false) == []`, no singleton methods at all. The distinction is
|
|
170
|
+
not pedantry: a file that requires `action_view` and never reaches into
|
|
171
|
+
`Helpers` does not arm this trap. Measured at this branch's head, a bare
|
|
87
172
|
`Studio::MailTransport.configure!(env: {}, action_mailer: mailer)` died with
|
|
88
173
|
`NoMethodError: undefined method 'env' for module Rails`. No shipped app can
|
|
89
174
|
reach it (every host configures mail from an initializer, where `Rails.env`
|
|
@@ -467,10 +552,12 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
467
552
|
long before any application does — ships a namespace-only `module Rails` with no
|
|
468
553
|
singleton methods, so that guard reads true and the next call raises
|
|
469
554
|
`NoMethodError: undefined method 'env' for module Rails`. It now asks
|
|
470
|
-
`Rails.respond_to?(:env)`, which is the form `lib/studio.rb`
|
|
471
|
-
places
|
|
472
|
-
|
|
473
|
-
|
|
555
|
+
`Rails.respond_to?(:env)`, which is the form `lib/studio.rb` now uses in FOUR
|
|
556
|
+
places: the three it already carried (lines 701, 707 and 742) plus the route
|
|
557
|
+
guard at line 859. **This entry originally called it "the straggler"; that was
|
|
558
|
+
wrong** — three more sites carried the bare form, line 859 among them, and all
|
|
559
|
+
three are swept in *The Rails guard sweep really is finished now* above, in this
|
|
560
|
+
same unreleased block. No shipped app can reach it — every host boots a
|
|
474
561
|
real application — but the engine's own pure-Ruby unit lane can, and it did:
|
|
475
562
|
adding one `require` for a file that needs `action_view` turned an untouched
|
|
476
563
|
`email_catalog_test` red with two errors about email uploads.
|
|
@@ -66,7 +66,25 @@
|
|
|
66
66
|
<table role="presentation" width="<%= banner.width %>" cellpadding="0" cellspacing="0" border="0"
|
|
67
67
|
style="width:<%= banner.width %>px;border-collapse:collapse;">
|
|
68
68
|
<tr>
|
|
69
|
-
|
|
69
|
+
<%# THE BACKGROUND ATTRIBUTE, EMITTED BY tag.attributes. It was assembled as a
|
|
70
|
+
Ruby string carrying its OWN quotes and then marked html_safe, which is the
|
|
71
|
+
one shape ERB's attribute escaping can never reach.
|
|
72
|
+
|
|
73
|
+
MEASURED on the unrepaired template, not reasoned about: a double quote in
|
|
74
|
+
background_url closed the attribute, and the browser read the remainder of
|
|
75
|
+
the tag as markup. The td came back carrying attributes named `quote"`,
|
|
76
|
+
`\`, `<` and `script`, and LOST bgcolor, width, height, valign and style
|
|
77
|
+
outright. That is worse than an injected attribute — the cell loses the
|
|
78
|
+
fallback colour and both dimensions the whole banner is laid out from, so
|
|
79
|
+
the failure is a wrecked email, not just a tainted one.
|
|
80
|
+
|
|
81
|
+
The URL is CONTENT, not code, so it is passed UNMARKED and takes the full
|
|
82
|
+
escaping a content attribute is owed. That also closes the latent half: a
|
|
83
|
+
bare ampersand in a signed-URL query string used to reach the client as the
|
|
84
|
+
start of an entity. Contrast the Alpine handlers elsewhere in this engine,
|
|
85
|
+
which stay html_safe precisely because they are expressions the host wrote
|
|
86
|
+
on purpose. The rule follows what the value IS, not which file it is in. %>
|
|
87
|
+
<td <%= tag.attributes(background: banner.background_url) if banner.background_url.present? %>
|
|
70
88
|
bgcolor="<%= Studio.theme_primary %>"
|
|
71
89
|
width="<%= banner.width %>" height="<%= banner.height %>" valign="middle"
|
|
72
90
|
style="width:<%= banner.width %>px;height:<%= banner.height %>px;<%= "background-image:url(#{banner.background_url});background-size:cover;background-position:center;background-repeat:no-repeat;" if banner.background_url.present? %>">
|
|
@@ -97,7 +115,24 @@
|
|
|
97
115
|
contrast case the scrim exists for, in the one client that
|
|
98
116
|
cannot be spot-checked. Every other client honours the rgba
|
|
99
117
|
declaration and overrides the flat attribute. %>
|
|
100
|
-
|
|
118
|
+
<%# SAME SHAPE AS THE background ABOVE, AND — stated honestly — NOT
|
|
119
|
+
THE SAME LIVE DEFECT. This one was assembled with its own quotes
|
|
120
|
+
and marked html_safe too, but the value cannot currently carry a
|
|
121
|
+
quote to exploit it: scrim_solid_hex formats three integers the
|
|
122
|
+
model has already clamped to 0-255, so it returns a hash sign
|
|
123
|
+
and six hex digits for EVERY input. Checked by rendering rather
|
|
124
|
+
than by reading, which is the only way that claim is worth
|
|
125
|
+
anything: a scrim of `0.5" onload="alert(1)` still came back as
|
|
126
|
+
the hex #4C4860, with all five of this cell's attributes intact.
|
|
127
|
+
|
|
128
|
+
It is converted anyway because the SHAPE is the hazard. Safety
|
|
129
|
+
here rests on an implementation detail one refactor away from
|
|
130
|
+
changing — a scrim that ever learned to accept a named colour
|
|
131
|
+
would reopen it silently — and leaving a hand-assembled
|
|
132
|
+
attribute beside a repaired one is how the wrong pattern gets
|
|
133
|
+
copied next. Same encoder, same reasoning, zero byte movement
|
|
134
|
+
for a hex triplet. %>
|
|
135
|
+
<%= tag.attributes(bgcolor: banner.scrim_solid_hex) if banner.scrim_opacity.positive? %>
|
|
101
136
|
style="height:<%= banner.height %>px;padding:0 <%= side_padding %>px;<%= "background-color:rgba(24,16,64,#{banner.scrim_opacity});" if banner.scrim_opacity.positive? %>">
|
|
102
137
|
<% if preview || banner.header.present? %>
|
|
103
138
|
<p<%= hook.call("header") %> style="margin:0 0 <%= gap_after_header %>px;font-family:Montserrat,'Segoe UI',Helvetica,Arial,sans-serif;font-size:<%= header_size %>px;line-height:<%= header_line %>px;font-weight:700;color:#ffffff;">
|
|
@@ -468,14 +468,6 @@
|
|
|
468
468
|
resolve_expr: "$store.dsModals.close()" %></div>
|
|
469
469
|
</template>
|
|
470
470
|
|
|
471
|
-
<%# wallet-setup — registered here with the other cards this page opens.
|
|
472
|
-
Its CARD lives in the Web3 section (it is about getting a wallet); the
|
|
473
|
-
3-of-3 pill it renders is the HOST's onboarding chain position, which
|
|
474
|
-
is not this page's business. The Onboarding section that used to own
|
|
475
|
-
it was folded into Profile on 2026-08-24. %>
|
|
476
|
-
<template x-if="$store.dsModals.current().id === 'wallet-setup'">
|
|
477
|
-
<div><%= render "style/modals/wallet_setup" %></div>
|
|
478
|
-
</template>
|
|
479
471
|
|
|
480
472
|
<%# --- Batch 3: the modals that had no card on EITHER page --- %>
|
|
481
473
|
<template x-if="$store.dsModals.current().id === 'it-begins'">
|
|
@@ -504,21 +496,12 @@
|
|
|
504
496
|
<template x-if="$store.dsModals.current().id === 'ds-onramp-hub'">
|
|
505
497
|
<div><%= render "style/modals/ds_onramp_hub" %></div>
|
|
506
498
|
</template>
|
|
507
|
-
<template x-if="$store.dsModals.current().id === 'ds-buy-entry-token'">
|
|
508
|
-
<div><%= render "style/modals/ds_buy_entry_token" %></div>
|
|
509
|
-
</template>
|
|
510
|
-
<template x-if="$store.dsModals.current().id === 'ds-cdp-ramp'">
|
|
511
|
-
<div><%= render "style/modals/ds_cdp_ramp" %></div>
|
|
512
|
-
</template>
|
|
513
499
|
<template x-if="$store.dsModals.current().id === 'ds-newsletter-success'">
|
|
514
500
|
<div><%= render "style/modals/ds_newsletter_success" %></div>
|
|
515
501
|
</template>
|
|
516
502
|
<template x-if="$store.dsModals.current().id === 'rate-limit-general'">
|
|
517
503
|
<div><%= render "style/modals/rate_limit_general" %></div>
|
|
518
504
|
</template>
|
|
519
|
-
<template x-if="$store.dsModals.current().id === 'wallet-changed'">
|
|
520
|
-
<div><%= render "style/modals/wallet_changed" %></div>
|
|
521
|
-
</template>
|
|
522
505
|
|
|
523
506
|
<%# --- Newsletter exit + the off-page quest celebration --- %>
|
|
524
507
|
<template x-if="$store.dsModals.current().id === 'quest-success'">
|
|
@@ -534,9 +517,6 @@
|
|
|
534
517
|
</template>
|
|
535
518
|
|
|
536
519
|
<%# --- Web3 --- %>
|
|
537
|
-
<template x-if="$store.dsModals.current().id === 'cosign-rejected'">
|
|
538
|
-
<div><%= render "style/modals/cosign_rejected" %></div>
|
|
539
|
-
</template>
|
|
540
520
|
<%# Both web3 sign-in cards render from SOLANA-STUDIO, and both are the
|
|
541
521
|
REAL shipped partials rather than style-guide forks — the same files
|
|
542
522
|
turf-monster renders in production, so what is reviewed here IS the
|
|
@@ -1187,9 +1167,9 @@
|
|
|
1187
1167
|
listed for reference but <strong>cannot be opened here</strong>. Nothing
|
|
1188
1168
|
registers them, so a trigger would open an empty panel.
|
|
1189
1169
|
<% end %>
|
|
1190
|
-
The cards run in the order a player meets them — <strong>
|
|
1191
|
-
prove it, spend from it</strong>: Connect wallet →
|
|
1192
|
-
|
|
1170
|
+
The cards run in the order a player meets them — <strong>connect a
|
|
1171
|
+
wallet, prove it, spend from it</strong>: Connect wallet → the two
|
|
1172
|
+
Sign Wallet states → Processing → success or error →
|
|
1193
1173
|
top-up → Entry confirmed. Inside that, <strong>Connect Wallet →
|
|
1194
1174
|
Processing on-chain transaction → On-chain success</strong> is a
|
|
1195
1175
|
<strong>walked flow</strong> and the live card <strong>glows</strong>,
|
|
@@ -1212,14 +1192,7 @@
|
|
|
1212
1192
|
<strong>standard web3 auth button</strong>: a wallet row (brand mark, the
|
|
1213
1193
|
wallet’s own name, Installed badge, chevron) rather than a filled
|
|
1214
1194
|
CTA, glowing via <code class="font-mono text-2xs">pulse-cta</code> because
|
|
1215
|
-
it is the one thing to press.
|
|
1216
|
-
Wallet</strong>, immediately before them: that one asks an account to
|
|
1217
|
-
<strong>get</strong> a wallet, these ask an account to
|
|
1218
|
-
<strong>prove</strong> the one it already has — opposite populations
|
|
1219
|
-
(<code class="font-mono text-2xs">WalletSetupPolicy</code> exits at its own
|
|
1220
|
-
step 1 for an account that holds a wallet), so no user ever meets both.
|
|
1221
|
-
They sit next to each other anyway, because the pair is easiest to keep
|
|
1222
|
-
straight when you can read them side by side.
|
|
1195
|
+
it is the one thing to press.
|
|
1223
1196
|
</p>
|
|
1224
1197
|
</div>
|
|
1225
1198
|
<%# ONE ordered run, read left to right as a player meets it: get a wallet,
|
|
@@ -1261,30 +1234,6 @@
|
|
|
1261
1234
|
section's business.
|
|
1262
1235
|
ONE card with a "Wallet detected" toggle rather than two cards sharing a
|
|
1263
1236
|
modal id: the two Sign Wallet cards below are what that shape costs. %>
|
|
1264
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1265
|
-
label: "Setup Wallet",
|
|
1266
|
-
reference: %(the "Set up your wallet" card (style/modals/_wallet_setup) — how an account with NO wallet gets one, and the host's third onboarding step (it renders a 3-of-3 pill, which is the host's chain position, not this section's). An APP-SPECIFIC flow: the engine owns only the chrome (shell, progress_pill, wallet_brand_sprite) and the house wallet-row idiom, while extension detection, the 90-second walkthrough video, the Detailed Guide route and the managed-wallet fallback stay in the host (turf-monster's own is ~576 lines). Toggle "Wallet detected" to swap the row between INSTALL and Installed. NOTE it is the OPPOSITE population to the two Sign Wallet cards immediately below — WalletSetupPolicy exits at its own step 1 for an account that already holds a wallet, so no user ever meets both. Open with $store.dsModals.open('wallet-setup', { detected: false })),
|
|
1267
|
-
card_data: "opts: { detected: false }",
|
|
1268
|
-
toggles: [{ model: "opts.detected", label: "Wallet detected" }],
|
|
1269
|
-
open_expr: "$store.dsModals.open('wallet-setup', { detected: opts.detected })",
|
|
1270
|
-
glow_when: ds_glow.call("wallet-setup"),
|
|
1271
|
-
disabled: !web3_on, openable: true } do %>
|
|
1272
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1273
|
-
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1274
|
-
<%# the 3-of-3 pill, filled — the whole reason this card sits here %>
|
|
1275
|
-
<span class="flex gap-1">
|
|
1276
|
-
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1277
|
-
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1278
|
-
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1279
|
-
</span>
|
|
1280
|
-
<span class="flex items-center gap-1.5 h-7 w-full rounded-lg border px-1.5" style="border-color: var(--color-border-strong)">
|
|
1281
|
-
<span class="w-4 h-4 rounded shrink-0" style="background: var(--color-primary)"></span>
|
|
1282
|
-
<span class="block h-1.5 w-8 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1283
|
-
</span>
|
|
1284
|
-
<%# the walkthrough placeholder, the card's second half %>
|
|
1285
|
-
<span class="block w-full rounded" style="height: 1.6rem; background: var(--color-text); opacity: .10"></span>
|
|
1286
|
-
</div>
|
|
1287
|
-
<% end %>
|
|
1288
1237
|
|
|
1289
1238
|
<%# SIGN WALLET — the web2-session-on-a-wallet-account card. NOT part of the
|
|
1290
1239
|
walk above: that walk is one on-chain transaction, this is an AUTH state
|
|
@@ -1411,20 +1360,6 @@
|
|
|
1411
1360
|
no — refusing to co-sign because the transaction it was handed did not
|
|
1412
1361
|
match the entry it prepared. They sit together because a reviewer
|
|
1413
1362
|
reading either alone would not know the other existed. %>
|
|
1414
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1415
|
-
label: "Co-sign refused",
|
|
1416
|
-
reference: %(the "Transaction not signed" card (style/modals/_cosign_rejected) — turf-monster's modals/_cosign_rejected, opened when POST confirm_onchain_entry answers 422 with code tx_rejected: the server refused to co-sign because the submitted transaction did not match the entry it had prepared. APP-SPECIFIC — the engine owns no entry preparation, no co-signing and no verdict; it lends card_header and nothing else. THE COPY IS DELIBERATELY VAGUE and should stay that way: good actors never see this card (it fires on a tampered or mismatched transaction), so naming WHICH server-side check failed would help an attacker probe the validation. What the copy does commit to is the part the one honest user needs — nothing was submitted, the funds are safe. Open with $store.dsModals.open('cosign-rejected')),
|
|
1417
|
-
open_expr: "$store.dsModals.open('cosign-rejected')",
|
|
1418
|
-
glow_when: ds_glow.call("cosign-rejected"),
|
|
1419
|
-
disabled: !web3_on, openable: true } do %>
|
|
1420
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1421
|
-
<div class="text-2xl leading-none">🛡️</div>
|
|
1422
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1423
|
-
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1424
|
-
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1425
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1426
|
-
</div>
|
|
1427
|
-
<% end %>
|
|
1428
1363
|
|
|
1429
1364
|
<%# Wallet deposit — standalone top-up card (not part of the walk). %>
|
|
1430
1365
|
<%= render layout: "style/modal_specimen", locals: {
|
|
@@ -1486,41 +1421,7 @@
|
|
|
1486
1421
|
</div>
|
|
1487
1422
|
<% end %>
|
|
1488
1423
|
|
|
1489
|
-
<%# ds-buy-entry-token — reached with a lineup picked and no way to pay. %>
|
|
1490
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1491
|
-
label: "Buy entry token",
|
|
1492
|
-
reference: %(the entry-token purchase (style/modals/_ds_buy_entry_token) — turf-monster's modals/_buy_entry_token, built from blocks/_close_x and two blocks/_rail_row. Reached when a player has picked a lineup and has no way to pay for it, which is why the copy says the lineup is SAVED: losing their picks is the one thing they are afraid of at that moment. The ranking is a PRODUCT decision rather than a visual one — Coinflow leads and carries the "New" badge because it mints a spendable entry token directly, the shortest path from here to being in the contest, while Stripe swaps into the auth wizard's tokens picker, the older and longer path, and defers. Open with $store.dsModals.open('ds-buy-entry-token')),
|
|
1493
|
-
open_expr: "$store.dsModals.open('ds-buy-entry-token')",
|
|
1494
|
-
glow_when: ds_glow.call("ds-buy-entry-token"),
|
|
1495
|
-
disabled: !web3_on, openable: true } do %>
|
|
1496
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1497
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1498
|
-
<span class="flex items-center gap-1.5 rounded-md p-1.5" style="border: 2px solid var(--color-primary)">
|
|
1499
|
-
<span class="block h-4 w-4 rounded" style="background: var(--color-cta)"></span>
|
|
1500
|
-
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1501
|
-
</span>
|
|
1502
|
-
<span class="flex items-center gap-1.5 rounded-md p-1.5 border border-subtle">
|
|
1503
|
-
<span class="block h-4 w-4 rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
1504
|
-
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1505
|
-
</span>
|
|
1506
|
-
</div>
|
|
1507
|
-
<% end %>
|
|
1508
1424
|
|
|
1509
|
-
<%# ds-cdp-ramp — ONE of thirteen states, and the card says so. %>
|
|
1510
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1511
|
-
label: "Coinbase ramp (preflight)",
|
|
1512
|
-
reference: %(the Coinbase ramp's BUY PREFLIGHT (style/modals/_ds_cdp_ramp) — turf-monster's modals/_cdp_ramp. DELIBERATELY PARTIAL: that modal is a TWO-FLOW step machine (buy and cash-out) across THIRTEEN states — preflight, opening, waiting, awaiting-cdp, send, settling and the rest — and this cards exactly one of them, the buy preflight, which is the state both funding cards hand off to and therefore the only one a player reaches without already being mid-flow. The other twelve are uncarded BY DECISION (operator, 2026-08-26), which is a different thing from an oversight — a cabinet that shows one state of a thirteen-state machine without admitting it teaches a reader that the machine is simpler than it is. /tasks/port-cdp-ramp-specimen holds the full state map if that decision reverses. Open with $store.dsModals.open('ds-cdp-ramp')),
|
|
1513
|
-
open_expr: "$store.dsModals.open('ds-cdp-ramp')",
|
|
1514
|
-
glow_when: ds_glow.call("ds-cdp-ramp"),
|
|
1515
|
-
disabled: !web3_on, openable: true } do %>
|
|
1516
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2 relative">
|
|
1517
|
-
<span class="absolute top-1 right-2 text-secondary leading-none">×</span>
|
|
1518
|
-
<span class="block h-2 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1519
|
-
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1520
|
-
<span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1521
|
-
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1522
|
-
</div>
|
|
1523
|
-
<% end %>
|
|
1524
1425
|
|
|
1525
1426
|
<%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
|
|
1526
1427
|
CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
|
|
@@ -1567,30 +1468,6 @@
|
|
|
1567
1468
|
</div>
|
|
1568
1469
|
<% end %>
|
|
1569
1470
|
|
|
1570
|
-
<%# wallet-changed — the extension switched accounts underneath the session.
|
|
1571
|
-
Both addresses are shown because recognising your own is the only way to
|
|
1572
|
-
tell a deliberate switch from an accident. %>
|
|
1573
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1574
|
-
label: "Wallet changed",
|
|
1575
|
-
reference: %(the Web3 "Wallet changed" reconciliation (style/modals/_wallet_changed) — turf-monster's modals/_wallet_changed, fired when the person switches accounts inside their wallet extension while signed in. Composes the engine's blocks/_card_header; the switch itself belongs to turf's wallet store (continueSwitch) and the engine owns no wallet. Nothing is broken — but the session and the extension now disagree about who this is, and every on-chain action from here would use an identity the page is not showing. So it prints BOTH addresses, truncated the Solana way, because recognising your own is the one check the app cannot make for you. Continuing re-derives a session against the new wallet, which is a round trip, so the button carries a busy state rather than looking dead. Open with $store.dsModals.open('wallet-changed', { currentAddress: '…', newAddress: '…' })),
|
|
1576
|
-
open_expr: "$store.dsModals.open('wallet-changed', { currentAddress: '7xKpDemoSignedInWallet9JZ2Q', newAddress: '9mQtDemoSwitchedWalletLd4V' })",
|
|
1577
|
-
glow_when: ds_glow.call("wallet-changed"),
|
|
1578
|
-
disabled: !web3_on, openable: true } do %>
|
|
1579
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1580
|
-
<div class="text-2xl leading-none">🔀</div>
|
|
1581
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1582
|
-
<%# the two truncated addresses — the comparison IS the card %>
|
|
1583
|
-
<span class="flex items-center justify-between gap-2">
|
|
1584
|
-
<span class="block h-1.5 w-10 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1585
|
-
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1586
|
-
</span>
|
|
1587
|
-
<span class="flex items-center justify-between gap-2">
|
|
1588
|
-
<span class="block h-1.5 w-10 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1589
|
-
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1590
|
-
</span>
|
|
1591
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1592
|
-
</div>
|
|
1593
|
-
<% end %>
|
|
1594
1471
|
</div>
|
|
1595
1472
|
</section>
|
|
1596
1473
|
|
data/lib/studio/js_literal.rb
CHANGED
|
@@ -56,6 +56,26 @@ module Studio
|
|
|
56
56
|
# the worked examples; test/lib/studio/attribute_encoding_contract_test.rb pins the
|
|
57
57
|
# ActionView behaviour the whole thing rests on.
|
|
58
58
|
#
|
|
59
|
+
# THE MARKING IS A JUDGEMENT ABOUT THE VALUE, NOT ABOUT THE FILE, and it is the
|
|
60
|
+
# half of that repair most easily got backwards. `tag.attributes` takes both kinds
|
|
61
|
+
# and does the right thing with each:
|
|
62
|
+
#
|
|
63
|
+
# EXPRESSION (an Alpine handler, a JS number, a store path) — pass it MARKED.
|
|
64
|
+
# Only the double quotes move, so the code the caller deliberately handed the
|
|
65
|
+
# engine still parses. Escaping it would be corruption of the same kind the
|
|
66
|
+
# script-body note above describes.
|
|
67
|
+
# CONTENT (a URL, a colour, a length, a regex, a tooltip sentence) — pass it
|
|
68
|
+
# UNMARKED and let it take the full escaping a content attribute is owed. The
|
|
69
|
+
# parser hands the value back decoded, and a bare `&` stops reaching the client
|
|
70
|
+
# as the start of an entity.
|
|
71
|
+
#
|
|
72
|
+
# Both repairs live in this engine and neither is the default: the four input
|
|
73
|
+
# constraints in modals/blocks/_leveling_activity are the content case beside the
|
|
74
|
+
# handlers in the same file, and studio/mailers/_layered_banner is the content case
|
|
75
|
+
# in an EMAIL, where the consumer is a mail client rather than a browser and the
|
|
76
|
+
# margin for a malformed attribute is smaller. Choosing by which file a value sits
|
|
77
|
+
# in, rather than by what the value is, gets one of those two wrong every time.
|
|
78
|
+
#
|
|
59
79
|
# AND IT IS THE WRONG REPAIR FOR IDENTIFIER POSITION. A local spliced in as a bare
|
|
60
80
|
# NAME — `$store.<%= modal_store %>.close()` — must be VALIDATED, never escaped:
|
|
61
81
|
# escape_javascript also escapes `$`, so a legitimate store name like `dsModals$2`
|
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.74.
|
|
4
|
+
version: 0.74.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -521,9 +521,6 @@ files:
|
|
|
521
521
|
- app/views/style/modals/_age_gate.html.erb
|
|
522
522
|
- app/views/style/modals/_auth.html.erb
|
|
523
523
|
- app/views/style/modals/_birthday.html.erb
|
|
524
|
-
- app/views/style/modals/_cosign_rejected.html.erb
|
|
525
|
-
- app/views/style/modals/_ds_buy_entry_token.html.erb
|
|
526
|
-
- app/views/style/modals/_ds_cdp_ramp.html.erb
|
|
527
524
|
- app/views/style/modals/_ds_close_x.html.erb
|
|
528
525
|
- app/views/style/modals/_ds_newsletter_success.html.erb
|
|
529
526
|
- app/views/style/modals/_ds_onramp_hub.html.erb
|
|
@@ -539,11 +536,9 @@ files:
|
|
|
539
536
|
- app/views/style/modals/_rate_limit_general.html.erb
|
|
540
537
|
- app/views/style/modals/_unsubscribe_confirm.html.erb
|
|
541
538
|
- app/views/style/modals/_unsubscribe_goodbye.html.erb
|
|
542
|
-
- app/views/style/modals/_wallet_changed.html.erb
|
|
543
539
|
- app/views/style/modals/_wallet_connect.html.erb
|
|
544
540
|
- app/views/style/modals/_wallet_connect_slot.html.erb
|
|
545
541
|
- app/views/style/modals/_wallet_deposit.html.erb
|
|
546
|
-
- app/views/style/modals/_wallet_setup.html.erb
|
|
547
542
|
- app/views/theme_settings/edit.html.erb
|
|
548
543
|
- app/views/user_mailer/magic_link.html.erb
|
|
549
544
|
- app/views/user_mailer/magic_link.text.erb
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
Living style guide specimen — the "we did not co-sign that" refusal.
|
|
3
|
-
|
|
4
|
-
APP-SPECIFIC FLOW, engine chrome only. What this card names is turf-monster's:
|
|
5
|
-
POST confirm_onchain_entry answered 422 with code `tx_rejected`, meaning the
|
|
6
|
-
server refused to co-sign because the submitted transaction did not match the
|
|
7
|
-
entry it had prepared. The engine owns no entry preparation, no co-signing and
|
|
8
|
-
no verdict — it lends `blocks/_card_header` and nothing else, exactly as
|
|
9
|
-
style/modals/_entry_tokens borrows chrome for a flow the engine does not own.
|
|
10
|
-
|
|
11
|
-
WHY THE COPY IS VAGUE, and why a reviewer should not "improve" it: good actors
|
|
12
|
-
never see this card. It fires on a tampered or mismatched transaction, so the
|
|
13
|
-
copy is deliberately reassuring and deliberately silent about WHICH
|
|
14
|
-
server-side check failed — naming the check would help an attacker probe the
|
|
15
|
-
validation. The reassurance ("nothing was submitted and your funds are safe")
|
|
16
|
-
is the part that matters to the one honest user who ever hits it.
|
|
17
|
-
|
|
18
|
-
Single root: the outer <div> is the host's required root.
|
|
19
|
-
%>
|
|
20
|
-
<div x-data="{}">
|
|
21
|
-
<%= render "studio/modals/blocks/card_header",
|
|
22
|
-
icon_emoji: "🛡️",
|
|
23
|
-
title: "Transaction not signed" do %>
|
|
24
|
-
<p class="text-sm text-body">
|
|
25
|
-
For your security we didn’t co-sign this transaction — it
|
|
26
|
-
didn’t match the entry we prepared, so nothing was submitted and your
|
|
27
|
-
funds are safe. Please close this and try entering again. If it keeps
|
|
28
|
-
happening, <span class="text-primary">contact support</span>.
|
|
29
|
-
</p>
|
|
30
|
-
<% end %>
|
|
31
|
-
|
|
32
|
-
<%# One button, because there is exactly one thing to do. The host also closes
|
|
33
|
-
on escape and click-outside; this card is dismissible by design. %>
|
|
34
|
-
<button type="button" @click="$store.dsModals.close()"
|
|
35
|
-
class="btn btn-primary btn-lg w-full">Got it</button>
|
|
36
|
-
</div>
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
Living style guide specimen — Buy an Entry Token.
|
|
3
|
-
|
|
4
|
-
APP-SPECIFIC flow, engine chrome throughout (blocks/_close_x + two
|
|
5
|
-
blocks/_rail_row). Reached when a player has picked a lineup and has no way to
|
|
6
|
-
pay for it, which is why the copy says the lineup is SAVED: the one thing they
|
|
7
|
-
are afraid of at that moment is losing their picks.
|
|
8
|
-
|
|
9
|
-
TWO RAILS, RANKED, and the ranking is a product decision rather than a visual
|
|
10
|
-
one. Coinflow leads and carries the "New" badge because it mints a spendable
|
|
11
|
-
entry token directly — it is the shortest path from here to being in the
|
|
12
|
-
contest. Stripe still swaps into the auth wizard's tokens picker, which is the
|
|
13
|
-
older, longer path, so it defers.
|
|
14
|
-
|
|
15
|
-
Single root: the outer <div> is the host's required root.
|
|
16
|
-
%>
|
|
17
|
-
<div class="relative">
|
|
18
|
-
<%= render "studio/modals/blocks/close_x", modal_store: "dsModals" %>
|
|
19
|
-
|
|
20
|
-
<div class="text-center pt-1 mb-2">
|
|
21
|
-
<h3 class="text-heading font-bold text-lg leading-tight">Buy an Entry Token</h3>
|
|
22
|
-
</div>
|
|
23
|
-
<p class="text-xs text-body mb-4 text-center">
|
|
24
|
-
1 token = 1 contest entry. Your lineup is saved — pick how to pay.
|
|
25
|
-
</p>
|
|
26
|
-
|
|
27
|
-
<div class="space-y-3">
|
|
28
|
-
<%= render "studio/modals/blocks/rail_row",
|
|
29
|
-
emphasis: :primary, on_click: "$store.dsModals.close()",
|
|
30
|
-
icon_label: "CF", icon_bg: "#14B8A6",
|
|
31
|
-
title: "Coinflow · $19", subtitle: "Buy 1 entry with a card · fastest",
|
|
32
|
-
badge: "New", data: { ds_rail: "coinflow" } %>
|
|
33
|
-
|
|
34
|
-
<%= render "studio/modals/blocks/rail_row",
|
|
35
|
-
on_click: "$store.dsModals.close()",
|
|
36
|
-
icon_label: "S", icon_bg: "#635BFF",
|
|
37
|
-
title: "Card · $19", subtitle: "Buy an entry token with a card",
|
|
38
|
-
data: { ds_rail: "stripe" } %>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
Living style guide specimen — the Coinbase ramp, BUY PREFLIGHT only.
|
|
3
|
-
|
|
4
|
-
DELIBERATELY PARTIAL, and saying so is the point. turf's modals/_cdp_ramp is a
|
|
5
|
-
TWO-FLOW step machine — buy and cash-out — across thirteen states: preflight,
|
|
6
|
-
opening, waiting, awaiting-cdp, send, settling and the rest. This cards ONE of
|
|
7
|
-
them: the buy preflight, which is the state both funding cards hand off to and
|
|
8
|
-
therefore the only one a player reaches without already being mid-flow.
|
|
9
|
-
|
|
10
|
-
The other twelve are uncarded BY DECISION (operator, 2026-08-26: minimal
|
|
11
|
-
migration is fine here). That is a different thing from an oversight, and the
|
|
12
|
-
reference blurb on this card says so, because a cabinet that shows one state of
|
|
13
|
-
a thirteen-state machine without admitting it teaches a reader that the machine
|
|
14
|
-
is simpler than it is. /tasks/port-cdp-ramp-specimen holds the full state map
|
|
15
|
-
if the decision reverses.
|
|
16
|
-
|
|
17
|
-
APP-SPECIFIC, engine chrome: blocks/_close_x plus the centred title this whole
|
|
18
|
-
funding family uses.
|
|
19
|
-
|
|
20
|
-
Single root: the outer <div> is the host's required root.
|
|
21
|
-
%>
|
|
22
|
-
<div class="relative">
|
|
23
|
-
<%= render "studio/modals/blocks/close_x", modal_store: "dsModals" %>
|
|
24
|
-
|
|
25
|
-
<div class="text-center pt-1 mb-3">
|
|
26
|
-
<h3 class="text-heading font-bold text-lg leading-tight">Buy USDC with Coinbase</h3>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<p class="text-xs text-body mb-4 text-center">
|
|
30
|
-
You’ll finish the purchase in a Coinbase tab. We’ll update here
|
|
31
|
-
automatically once the USDC lands in your wallet.
|
|
32
|
-
</p>
|
|
33
|
-
|
|
34
|
-
<button type="button" @click="$store.dsModals.close()"
|
|
35
|
-
class="btn btn-primary btn-lg w-full">Continue to Coinbase</button>
|
|
36
|
-
|
|
37
|
-
<button type="button" @click="$store.dsModals.close()"
|
|
38
|
-
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
39
|
-
Not now
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
Living style guide specimen — the wallet switched underneath the session.
|
|
3
|
-
|
|
4
|
-
APP-SPECIFIC, engine chrome only (`blocks/_card_header`). The switch itself is
|
|
5
|
-
the host's: turf's wallet store owns continueSwitch, and the engine owns no
|
|
6
|
-
wallet.
|
|
7
|
-
|
|
8
|
-
THE SITUATION IT NAMES: the person changed accounts inside their wallet
|
|
9
|
-
extension while signed in here. Nothing is broken and nobody did anything
|
|
10
|
-
wrong — but the session and the extension now disagree about who this is, and
|
|
11
|
-
every on-chain action from here would use an identity the page is not showing.
|
|
12
|
-
|
|
13
|
-
SO IT SHOWS BOTH ADDRESSES, truncated the way every Solana UI truncates them
|
|
14
|
-
(4 leading, 4 trailing). Recognising your own address is the only way to tell a
|
|
15
|
-
deliberate switch from an accident, and it is the one check the app cannot make
|
|
16
|
-
for them.
|
|
17
|
-
|
|
18
|
-
The busy state matters: continuing re-derives a session against the new wallet,
|
|
19
|
-
which is a round trip. Without it the button looks dead and gets double-clicked.
|
|
20
|
-
|
|
21
|
-
Single root; x-data single-quoted throughout.
|
|
22
|
-
%>
|
|
23
|
-
<div x-data="{
|
|
24
|
-
busy: false,
|
|
25
|
-
get props() { var c = $store.dsModals.current(); return (c && c.props) || {} },
|
|
26
|
-
short(addr) {
|
|
27
|
-
addr = addr || '';
|
|
28
|
-
return addr.length > 12 ? (addr.slice(0, 6) + '...' + addr.slice(-4)) : addr;
|
|
29
|
-
},
|
|
30
|
-
go() {
|
|
31
|
-
if (this.busy) return;
|
|
32
|
-
this.busy = true;
|
|
33
|
-
var s = this;
|
|
34
|
-
// Demo: the real card awaits the host's wallet store. Hold briefly so
|
|
35
|
-
// the pending state is visible rather than skipped.
|
|
36
|
-
setTimeout(function () { s.busy = false; $store.dsModals.close(); }, 600);
|
|
37
|
-
}
|
|
38
|
-
}">
|
|
39
|
-
<%= render "studio/modals/blocks/card_header",
|
|
40
|
-
icon_emoji: "🔀",
|
|
41
|
-
title: "Wallet changed" do %>
|
|
42
|
-
<p class="text-sm text-body">
|
|
43
|
-
Your wallet is now on a different account than the one you signed in with.
|
|
44
|
-
Continue with the new one, or switch back in your wallet.
|
|
45
|
-
</p>
|
|
46
|
-
<% end %>
|
|
47
|
-
|
|
48
|
-
<div class="rounded-lg border border-subtle bg-surface-alt p-3 mb-4 text-sm">
|
|
49
|
-
<div class="flex items-center justify-between gap-3">
|
|
50
|
-
<span class="text-secondary">Signed in as</span>
|
|
51
|
-
<span class="font-mono text-heading" x-text="short(props.currentAddress) || '7xKp...JZ2Q'"></span>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="flex items-center justify-between gap-3 mt-2">
|
|
54
|
-
<span class="text-secondary">Wallet now</span>
|
|
55
|
-
<span class="font-mono text-heading" x-text="short(props.newAddress) || '9mQt...Ld4V'"></span>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<button type="button" @click="go()" :disabled="busy"
|
|
60
|
-
class="btn btn-primary btn-lg w-full disabled:opacity-60">
|
|
61
|
-
<span x-show="!busy">Continue with this wallet</span>
|
|
62
|
-
<span x-show="busy" class="inline-flex items-center justify-center gap-2" style="display: none;">
|
|
63
|
-
<span class="spinner" aria-hidden="true"></span>
|
|
64
|
-
Switching…
|
|
65
|
-
</span>
|
|
66
|
-
</button>
|
|
67
|
-
|
|
68
|
-
<button type="button" @click="$store.dsModals.close()"
|
|
69
|
-
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">Not now</button>
|
|
70
|
-
</div>
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
Living style guide specimen — the onboarding chain's LAST step, "Set up your
|
|
3
|
-
wallet" (chain step 3 of 3).
|
|
4
|
-
|
|
5
|
-
This documents an APP-SPECIFIC flow. The engine deliberately does NOT own it:
|
|
6
|
-
turf-monster's app/views/modals/_wallet_setup.html.erb is ~576 lines of
|
|
7
|
-
extension-presence polling, a hosted 90-second walkthrough video, a Detailed
|
|
8
|
-
Guide route that resolves at render time, and a managed-wallet fallback for
|
|
9
|
-
grandfathered web2 accounts. None of that is shareable — it is one app's
|
|
10
|
-
answer to "how do we get this person a wallet".
|
|
11
|
-
|
|
12
|
-
What IS shared, and all this specimen composes, is the CHROME:
|
|
13
|
-
studio/modals/blocks/shell — title row + close
|
|
14
|
-
studio/modals/blocks/progress_pill — the 3-of-3 chain position
|
|
15
|
-
studio/modals/blocks/wallet_brand_sprite — inline brand marks, no per-app PNG
|
|
16
|
-
plus the house wallet-row idiom (brand tile, the wallet's own name, status on
|
|
17
|
-
the right, chevron) and pulse-cta, so a wallet reads identically here, in the
|
|
18
|
-
connect picker, and on the web3 step-up card.
|
|
19
|
-
|
|
20
|
-
ONE card, ONE id, a `detected` TOGGLE for the two ends of the real progression
|
|
21
|
-
(install -> waiting -> Installed -> connect). Deliberately not two specimens
|
|
22
|
-
sharing a modal id: that shape is what left the two web3-step-up cards unable
|
|
23
|
-
to tell each other apart in the glow, fixed in the same pass that added this.
|
|
24
|
-
|
|
25
|
-
WHO SEES IT — the half worth remembering. This card and the web3 step-up card
|
|
26
|
-
are OPPOSITE populations and no user can ever meet both. WalletSetupPolicy
|
|
27
|
-
exits at its own step 1 for an account that already holds a wallet; the step-up
|
|
28
|
-
card is only ever shown to accounts that DO hold one. "Get a wallet" vs "prove
|
|
29
|
-
the wallet you have".
|
|
30
|
-
|
|
31
|
-
Self-contained: no install detection, no video embed, no real routes. The
|
|
32
|
-
walkthrough is represented as a still placeholder rather than an <iframe>,
|
|
33
|
-
because the video is the app's asset and an embed here would reach off-host.
|
|
34
|
-
|
|
35
|
-
Single root: the outer <div> is the host's required root.
|
|
36
|
-
%>
|
|
37
|
-
<div x-data="{
|
|
38
|
-
get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
|
|
39
|
-
get detected() { return !!this.props.detected; }
|
|
40
|
-
}"
|
|
41
|
-
class="relative">
|
|
42
|
-
<%= render "studio/modals/blocks/wallet_brand_sprite" %>
|
|
43
|
-
|
|
44
|
-
<%= render layout: "studio/modals/blocks/shell",
|
|
45
|
-
locals: { title: "Set up your wallet", modal_store: "dsModals" } do %>
|
|
46
|
-
|
|
47
|
-
<%# Chain position — the HOST's, not this page's. It is the only one of the
|
|
48
|
-
three onboarding steps that fills the pill, which is why the pill is here
|
|
49
|
-
even though the card sits in the Web3 section beside the wallet cards. %>
|
|
50
|
-
<div class="mb-3">
|
|
51
|
-
<%= render "studio/modals/blocks/progress_pill", current: 3, total: 3 %>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<p class="text-sm text-body text-center mb-4">
|
|
55
|
-
This app runs on the Solana public blockchain; you will need a wallet to
|
|
56
|
-
enter contests and collect winnings.
|
|
57
|
-
</p>
|
|
58
|
-
|
|
59
|
-
<%# THE wallet row — the house idiom, glowing because it is the one thing to
|
|
60
|
-
press. Same shape the connect picker and the step-up card use. The right
|
|
61
|
-
side is the only part that moves: INSTALL before the extension exists,
|
|
62
|
-
an Installed badge once it answers. %>
|
|
63
|
-
<button type="button" @click="$store.dsModals.close()"
|
|
64
|
-
style="--pulse-cta-color: var(--color-primary); --pulse-cta-strength: 0.4; --pulse-cta-scale: 1.02"
|
|
65
|
-
class="pulse-cta w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition text-left">
|
|
66
|
-
<span class="w-9 h-9 rounded-lg overflow-hidden flex items-center justify-center shrink-0">
|
|
67
|
-
<svg class="w-9 h-9" aria-hidden="true"><use href="#se-wallet-phantom"></use></svg>
|
|
68
|
-
</span>
|
|
69
|
-
<span class="font-semibold text-heading">Phantom</span>
|
|
70
|
-
<span class="ml-auto flex items-center gap-2">
|
|
71
|
-
<span x-show="!detected" class="text-xs text-muted uppercase tracking-wide">Install</span>
|
|
72
|
-
<span x-show="detected" class="badge border-primary text-primary">Installed</span>
|
|
73
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-muted">
|
|
74
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
75
|
-
</svg>
|
|
76
|
-
</span>
|
|
77
|
-
</button>
|
|
78
|
-
|
|
79
|
-
<%# The teaching half. A brand-new player does not know what a wallet IS, so
|
|
80
|
-
the card carries its own explainer rather than sending them away to find
|
|
81
|
-
one. Represented, not embedded — see the header. %>
|
|
82
|
-
<div class="mt-4 pt-4 border-t border-strong">
|
|
83
|
-
<h4 class="text-heading font-bold text-sm mb-1">New to Solana 🤔</h4>
|
|
84
|
-
<p class="text-sm text-body mb-3">Set up your wallet in <strong>90 seconds</strong>.</p>
|
|
85
|
-
|
|
86
|
-
<div class="rounded-lg border border-subtle bg-inset aspect-video flex flex-col items-center justify-center gap-1">
|
|
87
|
-
<span class="text-2xl leading-none" aria-hidden="true">▶️</span>
|
|
88
|
-
<span class="text-2xs text-muted">walkthrough — the app supplies the video</span>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<button type="button" @click="$store.dsModals.close()"
|
|
92
|
-
class="btn btn-primary btn-lg w-full mt-3 flex items-center justify-center gap-2">
|
|
93
|
-
Detailed Guide
|
|
94
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
|
95
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
96
|
-
</svg>
|
|
97
|
-
</button>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<%# Dismissible by design, and the fine print says what dismissing costs. The
|
|
101
|
-
chain is skippable; the entry gate is not, and the card is honest that
|
|
102
|
-
the two are different things. %>
|
|
103
|
-
<button type="button" @click="$store.dsModals.close()"
|
|
104
|
-
class="block mx-auto mt-4 text-sm text-secondary hover:text-heading transition">
|
|
105
|
-
Maybe later
|
|
106
|
-
</button>
|
|
107
|
-
<p class="mt-2 text-center text-2xs text-muted">
|
|
108
|
-
Rather pay by card? Link a wallet first — an entry token needs
|
|
109
|
-
somewhere to land.
|
|
110
|
-
</p>
|
|
111
|
-
<% end %>
|
|
112
|
-
</div>
|