studio-engine 0.74.0 → 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 +196 -2
- data/app/views/components/_sidebar_panel.html.erb +13 -3
- data/app/views/studio/board/_card_shell.html.erb +14 -0
- data/app/views/studio/mailers/_layered_banner.html.erb +37 -2
- data/app/views/studio/modals/blocks/_leveling_activity.html.erb +23 -5
- data/app/views/studio/modals/blocks/_processing_card.html.erb +20 -1
- data/app/views/studio/modals/blocks/_success_card.html.erb +19 -2
- data/app/views/style/_modals.html.erb +4 -127
- data/lib/studio/js_literal.rb +33 -0
- data/lib/studio/mail_transport.rb +2 -2
- data/lib/studio/s3.rb +12 -4
- data/lib/studio/version.rb +1 -1
- data/lib/studio.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,196 @@ 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
|
+
|
|
87
|
+
- **A quote in a host local can no longer close an Alpine attribute and turn the
|
|
88
|
+
rest of the element into markup.** Four partials built an attribute — its OWN
|
|
89
|
+
QUOTES included — as a Ruby String and marked it `html_safe`, which is the one
|
|
90
|
+
shape ERB's attribute escaping can never reach: the marking tells ERB to stand
|
|
91
|
+
down, so a double quote in the local ended the attribute and the HTML parser read
|
|
92
|
+
the remainder as new attributes. Measured, not reasoned: one hostile
|
|
93
|
+
`auto_redirect_seconds` turned the success card's root into an element carrying
|
|
94
|
+
attributes named `quote"`, `\`, `<` and `script`, with its `class` swallowed
|
|
95
|
+
whole. This is strictly worse than the JS-string-literal class fixed in
|
|
96
|
+
*Host-supplied locals can no longer brick an Alpine component* below, which at
|
|
97
|
+
least stayed inside its attribute.
|
|
98
|
+
|
|
99
|
+
**THE REPAIR IS NOT ESCAPING.** These locals are Alpine EXPRESSIONS by contract —
|
|
100
|
+
the caller is handing the engine JS on purpose — so `escape_javascript` would
|
|
101
|
+
break the feature it was protecting. The partials stop writing the quotes and let
|
|
102
|
+
ActionView write them: `tag.attributes("x-init": expr.html_safe)`. `tag_option`
|
|
103
|
+
runs `gsub('"', """)` on the finished value UNCONDITIONALLY, on the line
|
|
104
|
+
AFTER the escape branch it skips for a marked String, so the expression keeps its
|
|
105
|
+
apostrophes, angle brackets and backslashes byte-for-byte and still cannot end the
|
|
106
|
+
attribute. `blocks/_rail_row:112` already did exactly this and was the worked
|
|
107
|
+
example.
|
|
108
|
+
|
|
109
|
+
**WHAT MOVED — twelve splices in four files.** `blocks/_success_card` (the
|
|
110
|
+
Ruby-built `x-data` countdown object, and the `x-init` call list that splices
|
|
111
|
+
`auto_redirect_url_key`), `blocks/_processing_card` (one assembled `x-data`/`x-init`
|
|
112
|
+
pair carrying TWO locals — `resolve_expr` and `min_duration`, which only LOOKS
|
|
113
|
+
numeric because just its guard calls `.to_i`), `blocks/_leveling_activity` (the
|
|
114
|
+
Next Quest `@click`, plus `minlength` / `maxlength` / `pattern` / `title` on the
|
|
115
|
+
form input), and `components/_sidebar_panel` (`@click.outside`,
|
|
116
|
+
`@keydown.escape.window` and `@turbo:before-cache.window` on the root of the shell
|
|
117
|
+
every link menu hangs from). No default or in-repo value contains a character tag
|
|
118
|
+
encoding touches, so every shipped page renders byte-for-byte what it did — a
|
|
119
|
+
render-level test asserts that directly.
|
|
120
|
+
|
|
121
|
+
**THE MARKING NOW FOLLOWS WHAT THE VALUE IS.** Code stays `html_safe` and reaches
|
|
122
|
+
the browser unchanged; CONTENT does not. The four input constraint attributes are
|
|
123
|
+
a length, a regex and a tooltip, so they take the full escaping a content
|
|
124
|
+
attribute is owed — which also closes a second latent bug the marking carried, a
|
|
125
|
+
bare `&` in a `title` or `pattern` reaching the browser as the start of an entity.
|
|
126
|
+
|
|
127
|
+
**TWO OF THE SIX SITES ON THE ORIGINAL LIST WERE ALREADY SAFE and were left
|
|
128
|
+
alone.** `blocks/_rail_row:112` routes its handler through `content_tag`, and
|
|
129
|
+
`board/_card_shell:54` escapes both halves of its arbitrary-attribute passthrough
|
|
130
|
+
through ERB. Both were re-verified by rendering them with a hostile value, not by
|
|
131
|
+
reading them. Filing a fix for either would have bought a guard that can never
|
|
132
|
+
bite.
|
|
133
|
+
|
|
134
|
+
**HOW THE GUARDS ASSERT.** `test/views/assembled_attribute_locals_test.rb` reads
|
|
135
|
+
every seam through Nokogiri's HTML5 parser — the algorithm a browser runs — and
|
|
136
|
+
asserts on the DECODED attribute value, never on the template's output bytes,
|
|
137
|
+
because ERB entity-escapes an unmarked value too and a raw-markup structural
|
|
138
|
+
assertion therefore cannot fail. (HTML5 rather than HTML4 on purpose: HTML4
|
|
139
|
+
silently drops an `@click`, so every Alpine assertion made through it reads nil on
|
|
140
|
+
a correct page.) Each seam makes two claims — the element carries exactly the
|
|
141
|
+
attributes a benign value gives it, and the browser recovers the whole expression
|
|
142
|
+
— plus a non-vacuity control that fails if the partial ever stops splicing the
|
|
143
|
+
local at all. All twelve were mutated back to their assembled form ONE AT A TIME
|
|
144
|
+
and each was killed by its own test; the shared `_processing_card` call was also
|
|
145
|
+
mutated per-local. `test/lib/studio/attribute_encoding_contract_test.rb` pins the
|
|
146
|
+
ActionView property the whole repair stands on, so a future Rails moving that
|
|
147
|
+
`gsub` reports itself as one dependency change rather than twelve partial bugs.
|
|
148
|
+
|
|
149
|
+
**STILL OPEN, and named rather than quietly folded in:**
|
|
150
|
+
`studio/mailers/_layered_banner:69,100` assemble `background=` and `bgcolor=` the
|
|
151
|
+
same way. Same shape, different family — email attributes, no Alpine, no JS — so
|
|
152
|
+
they are left for their own ticket rather than widened into this one.
|
|
153
|
+
|
|
154
|
+
- **The Rails guard sweep really is finished now, and a test says so instead of a
|
|
155
|
+
comment.** Fixing `Studio::S3` left THREE sites still guarding a Rails method
|
|
156
|
+
call on a bare `defined?(Rails)`: both keyword defaults in
|
|
157
|
+
`Studio::MailTransport.configure!` (`rails_env:` reading `Rails.env`, `logger:`
|
|
158
|
+
reading `Rails.logger`) and the developer-desk route guard in
|
|
159
|
+
`Studio.routes`. All three now ask `Rails.respond_to?` first.
|
|
160
|
+
|
|
161
|
+
**THE MAIL TRANSPORT ONE WAS ARMED, not theoretical.** `lib/studio.rb` requires
|
|
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
|
|
172
|
+
`Studio::MailTransport.configure!(env: {}, action_mailer: mailer)` died with
|
|
173
|
+
`NoMethodError: undefined method 'env' for module Rails`. No shipped app can
|
|
174
|
+
reach it (every host configures mail from an initializer, where `Rails.env`
|
|
175
|
+
exists, and all four in-repo callers pass `rails_env:` explicitly) — the cost
|
|
176
|
+
lands on the next gem unit test that omits the kwarg, which is the same
|
|
177
|
+
half-hour the `Studio::S3` fix already paid once.
|
|
178
|
+
|
|
179
|
+
**THE TWO DEFAULTS ARE SEPARATE STRAGGLERS**, and the first hid the second: Ruby
|
|
180
|
+
evaluates only the defaults a caller omitted, and `rails_env:` is declared above
|
|
181
|
+
`logger:`, so a bare call raised out of `Rails.env` and never reached
|
|
182
|
+
`Rails.logger`. `test/lib/studio/mail_transport_namespace_only_rails_test.rb`
|
|
183
|
+
supplies one argument and omits the other in each test, so each guard is pinned
|
|
184
|
+
independently rather than behind its neighbour.
|
|
185
|
+
|
|
186
|
+
**THE ROUTE GUARD IS PINNED BY A SCAN, deliberately.** `Studio.routes` only
|
|
187
|
+
loads inside a real Rails application, where `Rails.env` exists — so the
|
|
188
|
+
condition under test cannot be constructed where the file loads, and no
|
|
189
|
+
behavioural test can reach it. `test/lib/studio/rails_guard_sweep_test.rb` is
|
|
190
|
+
the tree-wide net instead, and it asserts both that it really read the files and
|
|
191
|
+
that its predicate still flags the original shape, so it cannot pass for free.
|
|
192
|
+
|
|
193
|
+
**The completeness claim itself was the other half of the bug.** `lib/studio/s3.rb`
|
|
194
|
+
and this changelog both said that fix "was the straggler" — while one of the
|
|
195
|
+
remaining three sat in `lib/studio.rb`, the very file the comment cited as
|
|
196
|
+
already clean. Both claims are corrected, and the comment now points at the test
|
|
197
|
+
rather than restating that the sweep is done.
|
|
198
|
+
|
|
9
199
|
- **The app census in these comments was short by the most important app.** Ten
|
|
10
200
|
sites said this engine is mounted by SIX apps and that THREE of them bundle no
|
|
11
201
|
`solana-studio`. Measured 2026-09-07 by the criterion that reproduces it — a
|
|
@@ -362,8 +552,12 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
362
552
|
long before any application does — ships a namespace-only `module Rails` with no
|
|
363
553
|
singleton methods, so that guard reads true and the next call raises
|
|
364
554
|
`NoMethodError: undefined method 'env' for module Rails`. It now asks
|
|
365
|
-
`Rails.respond_to?(:env)`, which is the form `lib/studio.rb`
|
|
366
|
-
places
|
|
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
|
|
367
561
|
real application — but the engine's own pure-Ruby unit lane can, and it did:
|
|
368
562
|
adding one `require` for a file that needs `action_view` turned an untouched
|
|
369
563
|
`email_catalog_test` red with two errors about email uploads.
|
|
@@ -34,9 +34,19 @@
|
|
|
34
34
|
<% if id.present? %>id="<%= id %>"<% end %>
|
|
35
35
|
x-cloak
|
|
36
36
|
x-show="<%= open %>"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
<%# THE THREE DISMISSAL HANDLERS, EMITTED BY tag.attributes. Each was built as
|
|
38
|
+
%(@name="#{action}").html_safe — the attribute's OWN quotes assembled in Ruby
|
|
39
|
+
and then marked safe, which is the one shape ERB's escaping can never reach. A
|
|
40
|
+
double quote in any of the three actions closed the attribute and everything
|
|
41
|
+
after it became markup on the panel's root element.
|
|
42
|
+
|
|
43
|
+
The actions stay html_safe: they are Alpine EXPRESSIONS the host wrote (see the
|
|
44
|
+
@click at the close button below, and `open` above). tag_option's
|
|
45
|
+
gsub('"', """) runs on the finished value whether or not it is marked, so
|
|
46
|
+
the expression reaches Alpine unchanged and cannot end the attribute. %>
|
|
47
|
+
<%= tag.attributes("@click.outside": outside_action.html_safe) if outside_action.present? %>
|
|
48
|
+
<%= tag.attributes("@keydown.escape.window": escape_action.html_safe) if escape_action.present? %>
|
|
49
|
+
<%= tag.attributes("@turbo:before-cache.window": close_action.html_safe) if close_action.present? %>
|
|
40
50
|
x-transition:enter="transition ease-out duration-300"
|
|
41
51
|
x-transition:enter-start="translate-x-full"
|
|
42
52
|
x-transition:enter-end="translate-x-0"
|
|
@@ -32,6 +32,20 @@
|
|
|
32
32
|
attrs: Hash of extra root attributes ({ "x-data" => "…", "data-foo" => 1 }),
|
|
33
33
|
values are HTML-escaped — the rebase seam for a card that carries
|
|
34
34
|
its own Alpine/data-* on the root.
|
|
35
|
+
|
|
36
|
+
NOT A CANDIDATE FOR THE ASSEMBLED-ATTRIBUTE SWEEP: nothing here is
|
|
37
|
+
marked html_safe, so ERB escapes both halves. A double quote in a
|
|
38
|
+
VALUE becomes " and the parser decodes it back — the value half
|
|
39
|
+
cannot break out. Verified 2026-09-08 by rendering, not reading.
|
|
40
|
+
|
|
41
|
+
THE NAME HALF IS A DIFFERENT QUESTION, and entity decoding is not
|
|
42
|
+
what answers it. html_escape does not touch a SPACE, and a space
|
|
43
|
+
ENDS an attribute name: a name of `data-y=1 x-init` renders as
|
|
44
|
+
`data-y=1 x-init="…"` and the parser builds a real SECOND attribute
|
|
45
|
+
— measured, an injected x-init Alpine will run. What makes this seam
|
|
46
|
+
safe today is that nothing passes attrs: at all — zero call sites in
|
|
47
|
+
studio-engine, turf-monster and mcritchie-studio. Its first caller
|
|
48
|
+
owes an allow-list on the NAME before this emits it.
|
|
35
49
|
%>
|
|
36
50
|
<%
|
|
37
51
|
id = local_assigns.fetch(:id)
|
|
@@ -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;">
|
|
@@ -138,10 +138,23 @@
|
|
|
138
138
|
<% if input %>
|
|
139
139
|
<input type="<%= input_type %>" x-model="value" class="input-field w-full"
|
|
140
140
|
placeholder="<%= placeholder %>"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
<%# FOUR CONSTRAINT ATTRIBUTES, ENCODED AND — unlike the Alpine
|
|
142
|
+
handlers in this file — DELIBERATELY NOT html_safe. Each was
|
|
143
|
+
assembled as `attr="#{value}"`.html_safe, which handed ERB a
|
|
144
|
+
String it must not touch, so a double quote in any of the four
|
|
145
|
+
closed the attribute and the rest became markup. minlength
|
|
146
|
+
looks numeric and is not: only the GUARD calls .to_i, the
|
|
147
|
+
interpolation splices the local raw.
|
|
148
|
+
|
|
149
|
+
These are CONTENT, not code — a length, a regex, a tooltip
|
|
150
|
+
sentence — so they take the full escaping a content attribute
|
|
151
|
+
is owed and the parser decodes it back. That also repairs a
|
|
152
|
+
second latent bug the marking carried: a bare & in a title or
|
|
153
|
+
pattern used to reach the browser as the start of an entity. %>
|
|
154
|
+
<%= tag.attributes(minlength: min_length) if min_length.to_i.positive? %>
|
|
155
|
+
<%= tag.attributes(maxlength: max_length) if max_length.present? %>
|
|
156
|
+
<%= tag.attributes(pattern: pattern) if pattern.present? %>
|
|
157
|
+
<%= tag.attributes(title: pattern_title) if pattern_title.present? %>
|
|
145
158
|
autofocus>
|
|
146
159
|
<% end %>
|
|
147
160
|
<% if consent_label.present? %>
|
|
@@ -180,7 +193,12 @@
|
|
|
180
193
|
</div>
|
|
181
194
|
|
|
182
195
|
<% if next_label.present? && next_open.present? %>
|
|
183
|
-
|
|
196
|
+
<%# next_open is html_safe (:102) because it is a developer-authored Alpine
|
|
197
|
+
EXPRESSION — so ERB steps aside here and a double quote in it used to
|
|
198
|
+
close the @click attribute outright. tag.attributes writes the quotes and
|
|
199
|
+
entity-encodes any of its own that appear in the value, leaving the
|
|
200
|
+
expression otherwise byte-identical. %>
|
|
201
|
+
<button type="button" <%= tag.attributes("@click": next_open) %> class="btn btn-primary btn-lg w-full">
|
|
184
202
|
<%= next_label %>
|
|
185
203
|
</button>
|
|
186
204
|
<% end %>
|
|
@@ -41,8 +41,27 @@
|
|
|
41
41
|
# JS sides of the convention agree on the same floor.
|
|
42
42
|
min_duration = local_assigns[:min_duration] || 1400
|
|
43
43
|
resolve_expr = local_assigns[:resolve_expr]
|
|
44
|
+
|
|
45
|
+
# THE AUTO-RESOLVE PAIR, ENCODED RATHER THAN ASSEMBLED. This line used to build
|
|
46
|
+
# ` x-data="{}" x-init="…"` as one html_safe String, quotes and all, so ERB had
|
|
47
|
+
# nothing left to escape: a double quote in EITHER local — min_duration as much as
|
|
48
|
+
# resolve_expr, since min_duration is interpolated raw and only its GUARD calls
|
|
49
|
+
# .to_i — closed the attribute and turned the remainder into markup.
|
|
50
|
+
#
|
|
51
|
+
# tag.attributes writes the quotes instead. The x-init value stays html_safe
|
|
52
|
+
# because it is CODE the caller handed us, and tag_option's unconditional
|
|
53
|
+
# gsub('"', """) still fires on an html_safe value, so the JS arrives intact
|
|
54
|
+
# and the attribute cannot end early. safe_join supplies the separating space
|
|
55
|
+
# without a second hand-marked String.
|
|
56
|
+
resolve_attrs =
|
|
57
|
+
if resolve_expr
|
|
58
|
+
safe_join([" ", tag.attributes(
|
|
59
|
+
"x-data": "{}",
|
|
60
|
+
"x-init": "window.StudioModals.holdAtLeast(#{min_duration}).then(() => { #{resolve_expr} })".html_safe
|
|
61
|
+
)])
|
|
62
|
+
end
|
|
44
63
|
%>
|
|
45
|
-
<div class="text-center py-6"<%=
|
|
64
|
+
<div class="text-center py-6"<%= resolve_attrs %>>
|
|
46
65
|
<div class="mx-auto <%= spinner_class %> rounded-full border-<%= color %>/30 border-t-<%= color %> animate-spin mb-5"></div>
|
|
47
66
|
<% if local_assigns[:title_key] %>
|
|
48
67
|
<p class="text-base font-bold text-heading mb-1" x-text="<%= title_key %>"></p>
|
|
@@ -122,9 +122,26 @@
|
|
|
122
122
|
init_calls = []
|
|
123
123
|
init_calls << "fireConfetti()" if fire_confetti
|
|
124
124
|
init_calls << "startCountdown(#{local_assigns[:auto_redirect_url_key]})" if has_redirect
|
|
125
|
+
init_expr = init_calls.join("; ").html_safe
|
|
125
126
|
%>
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
<%# BOTH ALPINE ATTRIBUTES ARE EMITTED BY tag.attributes, NOT ASSEMBLED BY HAND.
|
|
128
|
+
Each carries Ruby-built JS that splices a host local — redirect_secs into the
|
|
129
|
+
x-data object, auto_redirect_url_key into startCountdown() — and both blobs are
|
|
130
|
+
html_safe, because they are CODE and the caller is owed the JS it wrote.
|
|
131
|
+
|
|
132
|
+
THAT MARKING USED TO BE THE HOLE. html_safe tells ERB to step aside, so a double
|
|
133
|
+
quote in either local closed the ATTRIBUTE and everything after it became markup:
|
|
134
|
+
x-data="{ _remaining: it's a " quote" … — measured, not reasoned.
|
|
135
|
+
|
|
136
|
+
tag.attributes closes it without touching the JS. ActionView's tag_option runs
|
|
137
|
+
gsub('"', """) on the finished value UNCONDITIONALLY, outside the escape
|
|
138
|
+
branch it skips for an html_safe string (actionview tag_helper.rb, the line after
|
|
139
|
+
the escape ternary), so the attribute cannot be closed early and the HTML parser
|
|
140
|
+
decodes the entity back before Alpine ever sees it. Every other character reaches
|
|
141
|
+
the attribute byte-for-byte as it does today. blocks/_rail_row:112 is the same
|
|
142
|
+
move on a whole tag and its comment carries the rest of the reasoning. %>
|
|
143
|
+
<div <%= tag.attributes("x-data": data_attr) %>
|
|
144
|
+
<%= tag.attributes("x-init": init_expr) if init_calls.any? %>
|
|
128
145
|
class="text-center py-6">
|
|
129
146
|
|
|
130
147
|
<%# Icon — emoji takes priority over the default circular green check %>
|
|
@@ -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
|
@@ -43,6 +43,39 @@ module Studio
|
|
|
43
43
|
# safe). The name says `in_attribute` so that the next person reaching for this in
|
|
44
44
|
# a <script> has to stop and notice the difference.
|
|
45
45
|
#
|
|
46
|
+
# AND IT IS THE WRONG REPAIR FOR AN ATTRIBUTE THE PARTIAL ASSEMBLES ITSELF —
|
|
47
|
+
# `<%= "x-init=\"#{expr}\"".html_safe %>`, where the attribute's OWN QUOTES are
|
|
48
|
+
# built in Ruby. Nothing here helps: escaping the value would break the Alpine
|
|
49
|
+
# EXPRESSION the caller deliberately handed the engine, and the marking has already
|
|
50
|
+
# told ERB to stand down, so a double quote closes the ATTRIBUTE and the remainder
|
|
51
|
+
# is parsed as MARKUP. The repair is to stop writing the quotes and let ActionView
|
|
52
|
+
# write them — `tag.attributes("x-init": expr.html_safe)` — because tag_option
|
|
53
|
+
# quote-escapes the finished value UNCONDITIONALLY, outside the escape branch it
|
|
54
|
+
# skips for a marked String. The expression survives byte-for-byte and cannot end
|
|
55
|
+
# the attribute. modals/blocks/_rail_row:112 and components/_sidebar_panel carry
|
|
56
|
+
# the worked examples; test/lib/studio/attribute_encoding_contract_test.rb pins the
|
|
57
|
+
# ActionView behaviour the whole thing rests on.
|
|
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
|
+
#
|
|
46
79
|
# AND IT IS THE WRONG REPAIR FOR IDENTIFIER POSITION. A local spliced in as a bare
|
|
47
80
|
# NAME — `$store.<%= modal_store %>.close()` — must be VALIDATED, never escaped:
|
|
48
81
|
# escape_javascript also escapes `$`, so a legitimate store name like `dsModals$2`
|
|
@@ -6,9 +6,9 @@ module Studio
|
|
|
6
6
|
|
|
7
7
|
class << self
|
|
8
8
|
def configure!(env: ENV,
|
|
9
|
-
rails_env: defined?(Rails) ? Rails.env : "development",
|
|
9
|
+
rails_env: defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : "development",
|
|
10
10
|
action_mailer: defined?(ActionMailer) ? ActionMailer::Base : nil,
|
|
11
|
-
logger: defined?(Rails) ? Rails.logger : nil,
|
|
11
|
+
logger: defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil,
|
|
12
12
|
mailer_from: defined?(Studio) && Studio.respond_to?(:mailer_from) ? Studio.mailer_from : nil,
|
|
13
13
|
resend_loader: method(:load_resend!),
|
|
14
14
|
resend_configurer: method(:configure_resend!))
|
data/lib/studio/s3.rb
CHANGED
|
@@ -128,10 +128,18 @@ module Studio
|
|
|
128
128
|
# before any Rails APPLICATION does — defines a namespace-only `module
|
|
129
129
|
# Rails` with no singleton methods on it. Against that, a bare
|
|
130
130
|
# `defined?(Rails)` reads TRUE and the very next call dies with
|
|
131
|
-
# NoMethodError: undefined method `env` for module Rails.
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
131
|
+
# NoMethodError: undefined method `env` for module Rails. It turned an
|
|
132
|
+
# unrelated green suite red the first time a unit test pulled action_view
|
|
133
|
+
# in.
|
|
134
|
+
#
|
|
135
|
+
# THIS WAS NOT THE LAST ONE, though it was described that way when it
|
|
136
|
+
# landed. Three sites still carried the bare form afterwards — both
|
|
137
|
+
# keyword defaults in lib/studio/mail_transport.rb and the developer-desk
|
|
138
|
+
# route guard in lib/studio.rb — and the claim that the sweep was finished
|
|
139
|
+
# is how they survived a second review. The whole tree is swept now, and
|
|
140
|
+
# test/lib/studio/rails_guard_sweep_test.rb is what keeps it that way; do
|
|
141
|
+
# not restate completeness here, because a comment cannot notice the next
|
|
142
|
+
# straggler and that test can.
|
|
135
143
|
defined?(Rails) && Rails.respond_to?(:env) && Rails.env&.production? ? "production" : "dev"
|
|
136
144
|
end
|
|
137
145
|
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -856,7 +856,7 @@ module Studio
|
|
|
856
856
|
# Developer-desk tools. Drawn outside production, and each controller
|
|
857
857
|
# re-checks Studio.local_tool_enabled? per request (loopback only) — the
|
|
858
858
|
# route being absent is the outer gate, not the only one.
|
|
859
|
-
unless defined?(Rails) && Rails.env.production?
|
|
859
|
+
unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
|
|
860
860
|
get "_studio/local_emails", to: "studio/local_emails#index", as: :studio_local_emails
|
|
861
861
|
get "_studio/local_review", to: "studio/local_reviews#show", as: :studio_local_review
|
|
862
862
|
end
|
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>
|