studio-engine 0.52.3 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/app/controllers/studio/profiles_controller.rb +28 -0
- data/app/views/studio/profiles/_birthday_picker_script.html.erb +36 -8
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +23 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ce8edffaa3298d94b27113b1add2064ed036620a6c8372e84e947cfbeefb895
|
|
4
|
+
data.tar.gz: d72221add8e7af3e036cb616779bf59fb9428573b478ecda56ebd804300387f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f72e5c3f5c5e6c0f03deca8238fff54a8da590b384c0987689becee01ca791c645178ff32d5597664ed5320e5a82408f91d5d6b4cb3b8219be602536f6cb2dd7
|
|
7
|
+
data.tar.gz: d28e41b0a6c6c1a2b58fbcfe66787e2be955e353e101ac567d78d628f3de865dfbf81aeb30b3cf1796809fe087cee5e5bb2ab77317510f4ff951da5bea038d64
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,56 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`Studio.after_newsletter_change`** — a seam so a host can react to a
|
|
10
|
+
subscribe or unsubscribe without the engine knowing what it does.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
Studio.after_newsletter_change = lambda do |user, subscribed:, first_join:|
|
|
14
|
+
GrantWelcomeSeeds.call(user) if subscribed && first_join
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Why it exists, concretely:** turf-monster pays a 25-seed welcome bonus
|
|
19
|
+
on-chain, gated on `first_newsletter_join?` — literally
|
|
20
|
+
`joined_email_list_at.nil?`. The engine's subscribe action set that column and
|
|
21
|
+
told nobody, so a subscribe from `/profile` granted no seeds **and** made the
|
|
22
|
+
once-ever bonus unclaimable forever. Turf had to hold the newsletter row off
|
|
23
|
+
its profile page entirely; this is what lets it come back.
|
|
24
|
+
|
|
25
|
+
**`first_join:` is computed BEFORE the write**, because the write is what sets
|
|
26
|
+
the column — asked afterwards it is always false, and no host could ever pay a
|
|
27
|
+
welcome bonus.
|
|
28
|
+
|
|
29
|
+
**A raising callback cannot undo the subscription.** The subscription is the
|
|
30
|
+
durable fact and the reaction is not: turf's grant goes over RPC to a chain
|
|
31
|
+
that is sometimes unreachable, and a failed bonus must not cost someone their
|
|
32
|
+
place on the mailing list, or roll the write back inside `rescue_and_log`. It
|
|
33
|
+
is rescued and logged.
|
|
34
|
+
|
|
35
|
+
Fires on both verbs, so a host that only cares about joins ignores the flag.
|
|
36
|
+
The default is inert.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **The birthday calendar keeps the side it opened on.** `place()` runs on every
|
|
42
|
+
scroll and resize, and it also **chose** the side — so the popover snapped
|
|
43
|
+
between above and below the moment the available space crossed the threshold.
|
|
44
|
+
Measured on turf-monster at 60px scroll steps: ABOVE, ABOVE, ABOVE, ABOVE, then
|
|
45
|
+
below — a **254px jump between two steps**, mid-scroll, while the reader was
|
|
46
|
+
looking at it.
|
|
47
|
+
|
|
48
|
+
The side is now decided once, by `chooseSide()`, when the popover opens.
|
|
49
|
+
`place()` only moves it.
|
|
50
|
+
|
|
51
|
+
This is a different bug from the height fix that shipped alongside it: that one
|
|
52
|
+
corrected **where** a flipped popover lands, and never asked whether the side
|
|
53
|
+
should be re-decided at all. The flip spec added with it opens and asserts
|
|
54
|
+
once, so a side that changes **later** was invisible to it.
|
|
55
|
+
|
|
56
|
+
|
|
7
57
|
### Fixed
|
|
8
58
|
|
|
9
59
|
- **The birthday calendar measures itself instead of guessing, so a flipped
|
|
@@ -166,11 +166,17 @@ module Studio
|
|
|
166
166
|
current_user.email = value
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
# BEFORE THE WRITE, because the write is what sets the column — asking
|
|
170
|
+
# afterwards always answers false, and "is this their first ever join" is
|
|
171
|
+
# exactly the question a once-ever welcome bonus asks.
|
|
172
|
+
first_join = !Studio::Newsletter.ever_joined?(current_user)
|
|
173
|
+
|
|
169
174
|
rescue_and_log(target: current_user) do
|
|
170
175
|
# left_email_list_at is CLEARED rather than left in place. `subscribed?`
|
|
171
176
|
# compares the two dates, so a stale leave date in the future of the join
|
|
172
177
|
# would read as unsubscribed the moment the clock disagreed.
|
|
173
178
|
current_user.update!(joined_email_list_at: Time.current, left_email_list_at: nil)
|
|
179
|
+
notify_newsletter_change(subscribed: true, first_join: first_join)
|
|
174
180
|
redirect_to profile_path, notice: "You're on the list."
|
|
175
181
|
end
|
|
176
182
|
end
|
|
@@ -191,12 +197,34 @@ module Studio
|
|
|
191
197
|
|
|
192
198
|
rescue_and_log(target: current_user) do
|
|
193
199
|
current_user.update!(left_email_list_at: Time.current)
|
|
200
|
+
notify_newsletter_change(subscribed: false, first_join: false)
|
|
194
201
|
redirect_to profile_path, notice: "You've been unsubscribed."
|
|
195
202
|
end
|
|
196
203
|
end
|
|
197
204
|
|
|
198
205
|
private
|
|
199
206
|
|
|
207
|
+
# Tell the host, and NEVER let it break the subscription.
|
|
208
|
+
#
|
|
209
|
+
# The subscription is the durable fact; the reaction is not. turf-monster's
|
|
210
|
+
# callback grants seeds ON-CHAIN over RPC to a node that is sometimes
|
|
211
|
+
# unreachable, and a failed bonus must not cost someone their place on the
|
|
212
|
+
# mailing list — or, worse, roll the write back inside rescue_and_log and
|
|
213
|
+
# leave them looking unsubscribed.
|
|
214
|
+
#
|
|
215
|
+
# Rescued and logged rather than re-raised for that reason. The host's own
|
|
216
|
+
# controller already treats its grant as deferred and backfillable; this
|
|
217
|
+
# keeps that true when the same grant is reached from here.
|
|
218
|
+
def notify_newsletter_change(subscribed:, first_join:)
|
|
219
|
+
callback = Studio.after_newsletter_change
|
|
220
|
+
return unless callback.respond_to?(:call)
|
|
221
|
+
|
|
222
|
+
callback.call(current_user, subscribed: subscribed, first_join: first_join)
|
|
223
|
+
rescue StandardError => e
|
|
224
|
+
Rails.logger.warn("[studio][newsletter] after_newsletter_change raised for " \
|
|
225
|
+
"user=#{current_user&.id}: #{e.class}: #{e.message.to_s[0, 200]}")
|
|
226
|
+
end
|
|
227
|
+
|
|
200
228
|
# Would the page render this row for this viewer?
|
|
201
229
|
#
|
|
202
230
|
# A HIDDEN ROW IS NOT A GUARD. Studio::ProfileSections drops a row this host
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
window.studioBirthdayPicker = function (initial) {
|
|
24
24
|
return {
|
|
25
25
|
open: false,
|
|
26
|
+
// Which side this open chose. Set by chooseSide() when the popover opens and
|
|
27
|
+
// then left alone — see place() for why re-deciding it is the bug.
|
|
28
|
+
flipped: false,
|
|
26
29
|
value: initial || "",
|
|
27
30
|
viewYear: 0,
|
|
28
31
|
viewMonth: 0,
|
|
@@ -123,9 +126,13 @@
|
|
|
123
126
|
|
|
124
127
|
// Placed twice, deliberately. The first call stops the popover appearing
|
|
125
128
|
// at 0,0 for a frame; the second is the one that can MEASURE, because
|
|
126
|
-
// offsetHeight is 0 while x-show still has the element hidden
|
|
129
|
+
// offsetHeight is 0 while x-show still has the element hidden — and it is
|
|
130
|
+
// also the only place the SIDE is chosen, for the life of this open.
|
|
127
131
|
this.place();
|
|
128
|
-
this.$nextTick(function () {
|
|
132
|
+
this.$nextTick(function () {
|
|
133
|
+
this.chooseSide();
|
|
134
|
+
this.place();
|
|
135
|
+
}.bind(this));
|
|
129
136
|
},
|
|
130
137
|
|
|
131
138
|
// Placed from the trigger's rect because the popover is fixed. Flips above
|
|
@@ -151,16 +158,37 @@
|
|
|
151
158
|
var popover = this.$refs.popover;
|
|
152
159
|
var height = (popover && popover.offsetHeight) || 340;
|
|
153
160
|
var GAP = 4;
|
|
154
|
-
var below = window.innerHeight - r.bottom;
|
|
155
161
|
|
|
156
162
|
this.left = r.left;
|
|
157
163
|
this.width = r.width;
|
|
158
164
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
// THE SIDE IS NOT RE-DECIDED HERE. place() runs on every scroll and
|
|
166
|
+
// resize event, and choosing the side each time makes the popover SNAP
|
|
167
|
+
// between above and below the moment the available space crosses the
|
|
168
|
+
// threshold. Measured on turf-monster at 60px scroll steps: ABOVE, ABOVE,
|
|
169
|
+
// ABOVE, ABOVE, then below — a 254px jump between two steps, mid-scroll,
|
|
170
|
+
// while the reader was looking at it. That is what "the date picker
|
|
171
|
+
// freaks out on scroll" was.
|
|
172
|
+
//
|
|
173
|
+
// chooseSide() runs ONCE per open; this only moves the popover.
|
|
174
|
+
this.top = this.flipped ? r.top - height - GAP : r.bottom + GAP;
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Which side to open on, decided once and then left alone. Flips above only
|
|
178
|
+
// when there is not room below AND there is room above — with neither, it
|
|
179
|
+
// stays below, attached to its field rather than pinned to the top of the
|
|
180
|
+
// viewport.
|
|
181
|
+
chooseSide() {
|
|
182
|
+
var el = this.$refs.trigger;
|
|
183
|
+
if (!el) return;
|
|
184
|
+
|
|
185
|
+
var r = el.getBoundingClientRect();
|
|
186
|
+
var popover = this.$refs.popover;
|
|
187
|
+
var height = (popover && popover.offsetHeight) || 340;
|
|
188
|
+
var GAP = 4;
|
|
189
|
+
var below = window.innerHeight - r.bottom;
|
|
190
|
+
|
|
191
|
+
this.flipped = below < height + GAP && r.top > height + GAP;
|
|
164
192
|
}
|
|
165
193
|
};
|
|
166
194
|
};
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -29,6 +29,29 @@ module Studio
|
|
|
29
29
|
mattr_accessor :registration_params, default: [:name, :email, :password, :password_confirmation]
|
|
30
30
|
mattr_accessor :configure_new_user, default: ->(user) {}
|
|
31
31
|
mattr_accessor :configure_sso_user, default: ->(user) {}
|
|
32
|
+
|
|
33
|
+
# Called after a newsletter subscribe or unsubscribe on /profile, so a host can
|
|
34
|
+
# react without the engine knowing anything about what it does.
|
|
35
|
+
#
|
|
36
|
+
# THE REASON THIS EXISTS, stated concretely: turf-monster pays a 25-seed welcome
|
|
37
|
+
# bonus on-chain, gated on `first_newsletter_join?` — literally
|
|
38
|
+
# `joined_email_list_at.nil?`. The engine's subscribe action sets that column
|
|
39
|
+
# and told nobody, so a subscribe from /profile granted no seeds AND made the
|
|
40
|
+
# once-ever bonus unclaimable forever. Turf had to hold the row off its profile
|
|
41
|
+
# page entirely. This is what lets it come back.
|
|
42
|
+
#
|
|
43
|
+
# Studio.after_newsletter_change = lambda do |user, subscribed:, first_join:|
|
|
44
|
+
# GrantWelcomeSeeds.call(user) if subscribed && first_join
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# `first_join:` is computed BEFORE the write, because the write is what sets the
|
|
48
|
+
# column — asking afterwards always answers false.
|
|
49
|
+
#
|
|
50
|
+
# A raising callback CANNOT undo the subscription: the subscription is the
|
|
51
|
+
# durable fact and the reaction is not. Turf's grant goes over RPC to a chain
|
|
52
|
+
# that is sometimes unreachable, and a failed bonus must not cost someone their
|
|
53
|
+
# place on the mailing list.
|
|
54
|
+
mattr_accessor :after_newsletter_change, default: ->(_user, subscribed:, first_join:) {}
|
|
32
55
|
mattr_accessor :sso_logo, default: nil
|
|
33
56
|
mattr_accessor :wallet_address_method, default: nil
|
|
34
57
|
mattr_accessor :theme_logos, default: []
|