Skip to content

Commit a490c5b

Browse files
committed
Replace Mailchimp with a Flodesk integration
This will allow us to reduce emailing costs by a factor of 10.
1 parent 20adbdc commit a490c5b

File tree

12 files changed

+351
-142
lines changed

12 files changed

+351
-142
lines changed

Gemfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ gem 'tzinfo-data'
4848
gem 'chosen-rails'
4949
gem 'commonmarker'
5050

51-
gem 'gibbon', '~> 3.5.0'
51+
gem 'faraday'
5252

5353
gem 'stripe'
5454

@@ -84,6 +84,7 @@ group :development, :test do
8484
gem 'fabrication'
8585
gem 'faker'
8686
gem 'launchy'
87+
gem 'pry-rails'
8788
gem 'pry-byebug'
8889
gem 'pry-remote'
8990
gem 'rspec-collection_matchers'

Gemfile.lock

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,6 @@ GEM
171171
foreman (0.87.2)
172172
friendly_id (5.5.1)
173173
activerecord (>= 4.0.0)
174-
gibbon (3.5.0)
175-
faraday (>= 1.0)
176-
multi_json (>= 1.11.0)
177174
globalid (1.2.1)
178175
activesupport (>= 6.1)
179176
haml (6.3.0)
@@ -230,7 +227,6 @@ GEM
230227
mini_portile2 (2.8.7)
231228
minitest (5.25.1)
232229
msgpack (1.7.2)
233-
multi_json (1.15.0)
234230
multi_xml (0.6.0)
235231
net-http (0.4.1)
236232
uri
@@ -293,6 +289,8 @@ GEM
293289
pry-byebug (3.10.1)
294290
byebug (~> 11.0)
295291
pry (>= 0.13, < 0.15)
292+
pry-rails (0.3.11)
293+
pry (>= 0.13.0)
296294
pry-remote (0.1.8)
297295
pry (~> 0.9)
298296
slop (~> 3.0)
@@ -511,10 +509,10 @@ DEPENDENCIES
511509
dotenv-rails
512510
fabrication
513511
faker
512+
faraday
514513
font_awesome5_rails
515514
foreman
516515
friendly_id
517-
gibbon (~> 3.5.0)
518516
haml
519517
high_voltage
520518
icalendar
@@ -535,6 +533,7 @@ DEPENDENCIES
535533
pickadate-rails
536534
premailer-rails
537535
pry-byebug
536+
pry-rails
538537
pry-remote
539538
public_activity
540539
puma (~> 6.4)

app/views/admin/contacts/index.html.haml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
%th Sponsor
1515
%th Contact name
1616
%th Contact email
17-
%th Mailchimp
17+
%th Mailing list
1818
%tbody
1919
- @contacts.each do |contact|
2020
%tr
Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
1+
- content_for :head do
2+
:javascript
3+
(function(w, d, t, h, s, n) {
4+
w.FlodeskObject = n;
5+
var fn = function() {
6+
(w[n].q = w[n].q || []).push(arguments);
7+
};
8+
w[n] = w[n] || fn;
9+
var f = d.getElementsByTagName(t)[0];
10+
var v = '?v=' + Math.floor(new Date().getTime() / (120 * 1000)) * 60;
11+
var sm = d.createElement(t);
12+
sm.async = true;
13+
sm.type = 'module';
14+
sm.src = h + s + '.mjs' + v;
15+
f.parentNode.insertBefore(sm, f);
16+
var sn = d.createElement(t);
17+
sn.async = true;
18+
sn.noModule = true;
19+
sn.src = h + s + '.js' + v;
20+
f.parentNode.insertBefore(sn, f);
21+
})(window, document, 'script', 'https://assets.flodesk.com', '/universal', 'fd');
22+
123
.row.justify-content-md-center
224
.col-md-8
325
%h2= t('homepage.newsletter.title')
426
%p= t('homepage.newsletter.description')
527

6-
-# Mailchimp Signup Form
7-
#mc_embed_signup
8-
%form.validate#mc-embedded-subscribe-form{ action: "https://codebar.us8.list-manage.com/subscribe/post?u=b4652d85b385945c79f2ffa2e&amp;id=3798974cb3",
9-
method: "post",
10-
name: "mc-embedded-subscribe-form",
11-
target: "_blank",
12-
novalidate: true }
13-
#mc_embed_signup_scroll
14-
#indicates-required
15-
.mc-field-group.mb-3
16-
.d-flex.justify-content-between
17-
%label.form-label{ for: "mce-EMAIL" }
18-
Email Address
19-
%span.asterisk *
20-
%small
21-
%span.asterisk * indicates required
22-
%input.required.email.form-control#mce-EMAIL{ type: 'email', value: '', name: 'EMAIL' }
23-
24-
.clear#mce-responses
25-
.response#mce-error-response{ style: 'display:none' }
26-
.response#mce-success-response{ style: 'display:none' }
27-
28-
-# real people should not fill this in and expect good things - do not remove this or risk form bot signups
29-
%div{ style: 'position: absolute; left: -5000px;', 'aria-hidden': 'true' }
30-
%input{ type: 'text', name: 'b_b4652d85b385945c79f2ffa2e_3798974cb3', tabindex: '-1', value: '' }
31-
.d-flex.justify-content-between.align-items-center
32-
%input.btn.btn-primary#mc-embedded-subscribe{ type: 'submit', value: 'Subscribe', name: 'subscribe' }
28+
-# Flodesk signup form
29+
#fd-form-678b693a7ae9331608185173
30+
:javascript
31+
if (window.fd) window.fd('form', {
32+
formId: '678b693a7ae9331608185173',
33+
containerEl: '#fd-form-678b693a7ae9331608185173'
34+
});

config/initializers/flodesk.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require 'services/flodesk'
2+
3+
key = Rails.env.test? ? 'test' : ENV['FLODESK_KEY']
4+
5+
logger.warn 'Missing key for Flodesk' unless key
6+
7+
Flodesk::Client.api_key = key
8+
Flodesk::Client.complete_timeout = 15
9+
Flodesk::Client.open_timeout = 15

config/initializers/mailchimp.rb

Lines changed: 0 additions & 6 deletions
This file was deleted.

config/locales/en.yml

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,6 @@ en:
9999
workshop_email_subject: "Regarding hosting a workshop"
100100
chapters:
101101
title: "Chapters"
102-
newsletter:
103-
title: Subscribe to our newsletter
104-
description: Get regular updates about the codebar community. Be informed about upcoming events, jobs, scholarships and other learning opportunities.
105102
events:
106103
upcoming: "Upcoming Events"
107104
donation_platforms:
@@ -350,7 +347,7 @@ en:
350347
terms_and_conditions:
351348
title: 'Terms and Conditions'
352349
body: 'Before you can proceed you must first read our Code of Conduct and agree to abide by it.'
353-
link_text: 'Read codebar''s Code of Conduct'
350+
link_text: 'Read codebars Code of Conduct'
354351
accept: 'Accept'
355352
messages:
356353
notice: 'You have to accept the Terms and Conditions before you are able to proceed.'
@@ -461,11 +458,11 @@ en:
461458
unsubscribe: Unsubscribe from newsletter
462459
messages:
463460
mailing_list:
464-
subscribe: 'You have subscribed to codebar''s newsletter'
465-
unsubscribe: 'You have unsubscribed from codebar''s newsletter'
461+
subscribe: 'You have subscribed to codebars newsletter'
462+
unsubscribe: 'You have unsubscribed from codebars newsletter'
466463
group:
467-
subscribe: 'You have subscribed to %{chapter}''s %{role} group'
468-
unsubscribe: 'You have unsubscribed from %{chapter}''s %{role} group'
464+
subscribe: 'You have subscribed to %{chapter}s %{role} group'
465+
unsubscribe: 'You have unsubscribed from %{chapter}s %{role} group'
469466

470467
pages:
471468
cookies:
@@ -716,7 +713,7 @@ en:
716713

717714
labels:
718715
terms_and_conditions_form:
719-
terms: 'I have read codebar''s Code of Conduct and agree with all of its terms.'
716+
terms: 'I have read codebars Code of Conduct and agree with all of its terms.'
720717
workshop:
721718
description: Additional description
722719
virtual: Virtual workshop

lib/services/flodesk.rb

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
require 'faraday'
2+
3+
module Flodesk
4+
# Subscriber status
5+
ACTIVE = 'active'.freeze
6+
7+
class Client
8+
API_ENDPOINT = 'https://api.flodesk.com/v1/'
9+
DEFAULT_TIMEOUT = 60
10+
11+
class_attribute :api_key
12+
class_attribute :complete_timeout
13+
class_attribute :open_timeout
14+
15+
# Allows for setting these values in `config/initializers/flodesk.rb`
16+
class << self
17+
def api_key
18+
@@api_key
19+
end
20+
21+
def complete_timeout
22+
@@complete_timeout
23+
end
24+
25+
def open_timeout
26+
@@open_timeout
27+
end
28+
end
29+
30+
attr_accessor :api_endpoint
31+
attr_accessor :debug
32+
attr_accessor :logger
33+
34+
# We need 3 actions:
35+
#
36+
# 1. subscribe --> params(list_id, email, first_name, last_name)
37+
# Documentation: https://developers.flodesk.com/#tag/subscriber/operation/createOrUpdateSubscriber
38+
# Endpoint: https://api.flodesk.com/v1/subscribers
39+
#
40+
# 2. unsubscribe --> params(list_id, email)
41+
# Documentation: https://developers.flodesk.com/#tag/subscriber/operation/removeSubscriberFromSegments
42+
# Endpoint: https://api.flodesk.com/v1/subscribers/{id_or_email}/segments
43+
#
44+
# 3. subscribed? --> params(list_id, email)
45+
# Documentation: https://developers.flodesk.com/#tag/subscriber/operation/retrieveSubscriber
46+
# Endpoint: https://api.flodesk.com/v1/subscribers/{id_or_email}
47+
48+
def initialize(api_key: nil, api_endpoint: nil, complete_timeout: nil, open_timeout: nil, debug: false, logger: nil)
49+
@api_key = api_key || self.class.api_key || ENV['FLODESK_KEY']
50+
@api_key = @api_key.strip if @api_key
51+
52+
@complete_timeout = complete_timeout || self.class.complete_timeout || DEFAULT_TIMEOUT
53+
@open_timeout = open_timeout || self.class.open_timeout || DEFAULT_TIMEOUT
54+
55+
# TODO
56+
# @debug = debug || self.class.debug || false
57+
# @logger = logger || self.class.logger || ::Logger.new(STDOUT)
58+
end
59+
60+
def disabled?
61+
!@api_key
62+
end
63+
64+
def subscribe(email:, first_name:, last_name:, segment_ids:, double_optin: true)
65+
body = {
66+
email: email,
67+
first_name: first_name,
68+
last_name: last_name,
69+
segment_ids: segment_ids,
70+
double_optin: double_optin,
71+
}
72+
73+
request(:post, "subscribers", body)
74+
end
75+
76+
def unsubscribe(email:, segment_ids:)
77+
body = {
78+
segment_ids: segment_ids
79+
}
80+
81+
response = request(:delete, "subscribers/#{email}/segments", body)
82+
end
83+
84+
def subscribed?(email:, segment_ids:)
85+
response = request(:get, "subscribers/#{email}")
86+
body = OpenStruct.new(response[:body])
87+
88+
# If not subscribed, stop here
89+
is_active = body.status.to_s.eql?(ACTIVE)
90+
return false unless is_active
91+
92+
segment_ids.all? do |segment_id|
93+
body.segments.any? { |segment| segment_id.to_s.eql?(segment["id"]) }
94+
end
95+
end
96+
97+
private
98+
99+
def connection
100+
@connection ||= begin
101+
options = {
102+
headers: {
103+
user_agent: "codebar (codebar.io)",
104+
},
105+
request: {
106+
timeout: @complete_timeout,
107+
open_timeout: @open_timeout,
108+
},
109+
}
110+
111+
# Config options: https://lostisland.github.io/faraday/#/customization/request-options
112+
Faraday.new(url: API_ENDPOINT, **options) do |config|
113+
config.request :json
114+
115+
# Beware: the order of these lines matter. Examples:
116+
# - https://mattbrictson.com/blog/advanced-http-techniques-in-ruby#pitfall-raise_error-and-logger-in-the-wrong-order
117+
# - https://stackoverflow.com/a/67182791/590525
118+
config.response :raise_error
119+
config.response :logger, Rails.logger, headers: true, bodies: true, log_level: :debug
120+
config.response :json
121+
122+
# https://developers.flodesk.com/#section/Authentication/api_key
123+
config.request :authorization, 'Basic', -> { @api_key }
124+
end
125+
end
126+
end
127+
128+
def request(http_method, endpoint, body = {})
129+
# Faraday's `delete` does not accept body at the time of writing
130+
response = if http_method == :delete
131+
connection.run_request(http_method, endpoint, body, nil)
132+
else
133+
connection.public_send(http_method, endpoint, body)
134+
end
135+
136+
{
137+
status: response.status,
138+
body: JSON.parse(response.body)
139+
}
140+
rescue Faraday::Error => err
141+
message = err.response_body["message"]
142+
143+
FlodeskError.new(message, {
144+
raw_body: err.response_body,
145+
status_code: err.response_status
146+
})
147+
end
148+
end
149+
150+
# Inspired by https://github.com/amro/gibbon/blob/master/lib/gibbon/mailchimp_error.rb
151+
class FlodeskError < StandardError
152+
attr_reader :status_code, :raw_body
153+
154+
def initialize(message = "", params = {})
155+
@status_code = params[:status_code]
156+
@raw_body = params[:raw_body]
157+
158+
super(message)
159+
end
160+
161+
def to_s
162+
super + " " + instance_variables_to_s
163+
end
164+
165+
private
166+
167+
def instance_variables_to_s
168+
[:status_code, :raw_body].map do |attr|
169+
attr_value = send(attr)
170+
171+
"@#{attr}=#{attr_value.inspect}"
172+
end.join(", ")
173+
end
174+
end
175+
end

0 commit comments

Comments
 (0)