studio-engine 0.51.0 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +108 -0
- data/app/assets/javascripts/studio/alpine.js +16 -0
- data/app/controllers/studio/profiles_controller.rb +53 -0
- data/app/views/layouts/studio/_head.html.erb +20 -1
- data/app/views/studio/profiles/_birthday_fields.html.erb +210 -12
- data/app/views/studio/profiles/_birthday_picker_script.html.erb +144 -0
- data/app/views/studio/profiles/_identity.html.erb +22 -1
- data/app/views/studio/profiles/_identity_body.html.erb +52 -23
- data/app/views/studio/profiles/_identity_styles.html.erb +76 -15
- data/app/views/studio/profiles/_newsletter_modals.html.erb +61 -0
- data/app/views/studio/profiles/_newsletter_section.html.erb +65 -0
- data/app/views/studio/profiles/edit.html.erb +26 -9
- data/app/views/studio/profiles/show.html.erb +35 -0
- data/lib/studio/engine.rb +1 -0
- data/lib/studio/newsletter.rb +70 -0
- data/lib/studio/profile_sections.rb +33 -6
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +9 -0
- metadata +6 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The newsletter subscription's rules. Pure Ruby and duck-typed, like
|
|
4
|
+
# Studio::OauthIdentity and Studio::ProfileImage — it loads without Rails, takes
|
|
5
|
+
# any object that answers the readers, and the unit suite exercises it without
|
|
6
|
+
# booting the dummy app.
|
|
7
|
+
#
|
|
8
|
+
# TWO TIMESTAMPS, NOT A BOOLEAN, and this is the decision the rest of the file
|
|
9
|
+
# follows from. `joined_email_list_at` and `left_email_list_at` are lifted from
|
|
10
|
+
# turf-monster, which has run this shape in production, and the pair carries
|
|
11
|
+
# three states a boolean cannot:
|
|
12
|
+
#
|
|
13
|
+
# never asked both nil — distinct from "unsubscribed"
|
|
14
|
+
# subscribed joined after left — including a REJOIN, where both are set
|
|
15
|
+
# unsubscribed left after joined — and the leave date survives the rejoin
|
|
16
|
+
#
|
|
17
|
+
# The third row is why turf keeps them: its 25-seed welcome bonus is once-ever,
|
|
18
|
+
# guarded on-chain, and "have they EVER joined" is a different question from "are
|
|
19
|
+
# they on the list today". A boolean answers neither. None of turf's seed
|
|
20
|
+
# machinery belongs in the engine — but the state shape does, because a consumer
|
|
21
|
+
# that adopts this and later wants that rule must not have to migrate away from a
|
|
22
|
+
# flag first.
|
|
23
|
+
module Studio
|
|
24
|
+
module Newsletter
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# The columns a host must have for any of this to mean anything. The profile
|
|
28
|
+
# row declares the same pair in `requires:`, so a host without them silently
|
|
29
|
+
# gets no newsletter card rather than a NoMethodError on every page load.
|
|
30
|
+
COLUMNS = %i[joined_email_list_at left_email_list_at].freeze
|
|
31
|
+
|
|
32
|
+
def serves?(user)
|
|
33
|
+
return false if user.nil?
|
|
34
|
+
|
|
35
|
+
COLUMNS.all? { |column| user.respond_to?(column) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# On the list right now.
|
|
39
|
+
#
|
|
40
|
+
# `joined > left` rather than `left.nil?`, because a REJOIN leaves both set
|
|
41
|
+
# and the later date wins. Reading only `left_email_list_at.nil?` would call
|
|
42
|
+
# every rejoined account unsubscribed forever.
|
|
43
|
+
def subscribed?(user)
|
|
44
|
+
return false unless serves?(user)
|
|
45
|
+
|
|
46
|
+
joined = user.joined_email_list_at
|
|
47
|
+
return false if joined.nil?
|
|
48
|
+
|
|
49
|
+
left = user.left_email_list_at
|
|
50
|
+
left.nil? || joined > left
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Has this account EVER been on the list? Distinct from `subscribed?`, and the
|
|
54
|
+
# question a once-ever welcome bonus asks. The engine does not pay bonuses; it
|
|
55
|
+
# answers the question so a consumer that does never has to reach past this
|
|
56
|
+
# module into the columns.
|
|
57
|
+
def ever_joined?(user)
|
|
58
|
+
serves?(user) && !user.joined_email_list_at.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A newsletter needs somewhere to send. An account with no address on file —
|
|
62
|
+
# a wallet-only sign-in, which turf-monster has plenty of — has to supply one
|
|
63
|
+
# before it can subscribe, so the UI asks rather than failing the submit.
|
|
64
|
+
def needs_email?(user)
|
|
65
|
+
return false unless serves?(user)
|
|
66
|
+
|
|
67
|
+
!user.respond_to?(:email) || user.email.to_s.strip.empty?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# REQUIRED HERE, not left to lib/studio.rb's ordering. DEFAULTS names
|
|
4
|
+
# Studio::Newsletter::COLUMNS at LOAD time, and profile_sections is required
|
|
5
|
+
# BEFORE newsletter in that file — so without this line the constant is undefined
|
|
6
|
+
# and the engine fails to boot. Declaring the dependency where it is used makes
|
|
7
|
+
# the pair immune to a future reorder of that require list.
|
|
8
|
+
require_relative "newsletter"
|
|
9
|
+
|
|
3
10
|
# Resolves Studio.profile_sections — the rows that make up the shared /profile
|
|
4
11
|
# page — for a given view context. Pure Ruby (no Rails dependency) so the unit
|
|
5
12
|
# suite exercises the resolution rules without booting the dummy app.
|
|
@@ -33,12 +40,19 @@
|
|
|
33
40
|
# AND `auth_methods = %i[magic_link]` with no omniauth gem at all,
|
|
34
41
|
# so it would have rendered a "Link Google Account" button leading
|
|
35
42
|
# nowhere. Having a column is not the same as offering the feature.
|
|
36
|
-
# modals —
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# the engine's shared host
|
|
43
|
+
# modals — a partial of modal registrations this row needs, or nil. The PAGE
|
|
44
|
+
# mounts studio/modals/_scoped_host on a "profileModals" store when
|
|
45
|
+
# ANY resolved row declares one, and renders each declared partial
|
|
46
|
+
# inside it — so a row never has to know whether the host app renders
|
|
47
|
+
# a modal host of its own. Two of the five consumers render none at
|
|
48
|
+
# all, and the two that do ship a FORK of the engine's shared host
|
|
49
|
+
# that would shadow it.
|
|
50
|
+
#
|
|
51
|
+
# A PATH, not a boolean, and not derived from `key`. A host row
|
|
52
|
+
# declaring modals keeps its partial in the host's own app, so any
|
|
53
|
+
# convention like "studio/profiles/#{key}_modals" would resolve to a
|
|
54
|
+
# path that does not exist there. Saying it outright costs one string
|
|
55
|
+
# and works for everyone.
|
|
42
56
|
#
|
|
43
57
|
# ON `requires` — this is the whole reason the registry exists rather than a
|
|
44
58
|
# hardcoded page. The consuming apps do NOT agree on their users table:
|
|
@@ -73,6 +87,19 @@ module Studio
|
|
|
73
87
|
partial: "studio/profiles/google_section", requires: %i[provider uid],
|
|
74
88
|
if: -> { Studio.auth_method?(:google) } },
|
|
75
89
|
|
|
90
|
+
# The mailing list. READ-level for the same reason Google is: it is a state
|
|
91
|
+
# you look at and occasionally flip, not a field you type into.
|
|
92
|
+
#
|
|
93
|
+
# `modals: true` is what makes the page mount a host — leaving asks for
|
|
94
|
+
# confirmation, and an account with no address on file is asked for one.
|
|
95
|
+
# This is the FIRST row to declare the flag, which the registry has
|
|
96
|
+
# documented since it shipped; before this the read page mounted no host at
|
|
97
|
+
# all, correctly, because nothing asked.
|
|
98
|
+
{ key: :newsletter, title: "Newsletter", page: :show,
|
|
99
|
+
partial: "studio/profiles/newsletter_section",
|
|
100
|
+
requires: Studio::Newsletter::COLUMNS,
|
|
101
|
+
modals: "studio/profiles/newsletter_modals" },
|
|
102
|
+
|
|
76
103
|
# --- the edit page ------------------------------------------------------
|
|
77
104
|
# These are FIELDS in one form with one Save, so they carry no buttons of
|
|
78
105
|
# their own. Email included: it needed a separate action only while it was
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "studio/sidebar_sections"
|
|
|
10
10
|
require "studio/profile_sections"
|
|
11
11
|
require "studio/profile_image"
|
|
12
12
|
require "studio/oauth_identity"
|
|
13
|
+
require "studio/newsletter"
|
|
13
14
|
require "studio/username_generator"
|
|
14
15
|
require "studio/s3"
|
|
15
16
|
require "studio/image_cache"
|
|
@@ -690,6 +691,14 @@ module Studio
|
|
|
690
691
|
# here: it is OmniAuth's own /auth/:provider, which the middleware owns.
|
|
691
692
|
delete "profile/google", to: "studio/profiles#unlink_google",
|
|
692
693
|
as: :profile_unlink_google
|
|
694
|
+
|
|
695
|
+
# POST joins, DELETE leaves — the verbs the two actions actually are, on
|
|
696
|
+
# one path. Not a PATCH on the profile: subscribing is its own decision
|
|
697
|
+
# with its own confirmation, and folding it into the bulk field save would
|
|
698
|
+
# mean every name change re-asserted a mailing-list preference.
|
|
699
|
+
post "profile/newsletter", to: "studio/profiles#subscribe_newsletter",
|
|
700
|
+
as: :profile_newsletter
|
|
701
|
+
delete "profile/newsletter", to: "studio/profiles#unsubscribe_newsletter"
|
|
693
702
|
end
|
|
694
703
|
|
|
695
704
|
resources :error_logs, only: [:index, :show]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.52.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -175,6 +175,7 @@ files:
|
|
|
175
175
|
- app/assets/images/emails/newsletter-subscribed-background.gif
|
|
176
176
|
- app/assets/images/resend-favicon.png
|
|
177
177
|
- app/assets/images/ses-favicon.png
|
|
178
|
+
- app/assets/javascripts/studio/alpine.js
|
|
178
179
|
- app/assets/javascripts/studio/canvas_confetti.js
|
|
179
180
|
- app/assets/javascripts/studio/sortable.js
|
|
180
181
|
- app/assets/javascripts/studio/sticky_table_header.js
|
|
@@ -339,6 +340,7 @@ files:
|
|
|
339
340
|
- app/views/studio/profile_mailer/email_change_notification.html.erb
|
|
340
341
|
- app/views/studio/profile_mailer/email_change_notification.text.erb
|
|
341
342
|
- app/views/studio/profiles/_birthday_fields.html.erb
|
|
343
|
+
- app/views/studio/profiles/_birthday_picker_script.html.erb
|
|
342
344
|
- app/views/studio/profiles/_editable_identity.html.erb
|
|
343
345
|
- app/views/studio/profiles/_email_fields.html.erb
|
|
344
346
|
- app/views/studio/profiles/_form_script.html.erb
|
|
@@ -348,6 +350,8 @@ files:
|
|
|
348
350
|
- app/views/studio/profiles/_identity_mini.html.erb
|
|
349
351
|
- app/views/studio/profiles/_identity_styles.html.erb
|
|
350
352
|
- app/views/studio/profiles/_name_fields.html.erb
|
|
353
|
+
- app/views/studio/profiles/_newsletter_modals.html.erb
|
|
354
|
+
- app/views/studio/profiles/_newsletter_section.html.erb
|
|
351
355
|
- app/views/studio/profiles/_pencil_icon.html.erb
|
|
352
356
|
- app/views/studio/profiles/_save_bar.html.erb
|
|
353
357
|
- app/views/studio/profiles/edit.html.erb
|
|
@@ -392,6 +396,7 @@ files:
|
|
|
392
396
|
- lib/studio/link_token.rb
|
|
393
397
|
- lib/studio/log_rotation.rb
|
|
394
398
|
- lib/studio/mail_transport.rb
|
|
399
|
+
- lib/studio/newsletter.rb
|
|
395
400
|
- lib/studio/oauth_identity.rb
|
|
396
401
|
- lib/studio/profile_image.rb
|
|
397
402
|
- lib/studio/profile_sections.rb
|