Skip to content

Commit fc6ba1b

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

File tree

12 files changed

+348
-139
lines changed

12 files changed

+348
-139
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.88.1)
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)
@@ -231,7 +228,6 @@ GEM
231228
mini_portile2 (2.8.8)
232229
minitest (5.25.1)
233230
msgpack (1.7.2)
234-
multi_json (1.15.0)
235231
multi_xml (0.6.0)
236232
net-http (0.4.1)
237233
uri
@@ -294,6 +290,8 @@ GEM
294290
pry-byebug (3.10.1)
295291
byebug (~> 11.0)
296292
pry (>= 0.13, < 0.15)
293+
pry-rails (0.3.11)
294+
pry (>= 0.13.0)
297295
pry-remote (0.1.8)
298296
pry (~> 0.9)
299297
slop (~> 3.0)
@@ -514,10 +512,10 @@ DEPENDENCIES
514512
dotenv-rails
515513
fabrication
516514
faker
515+
faraday
517516
font_awesome5_rails
518517
foreman
519518
friendly_id
520-
gibbon (~> 3.5.0)
521519
haml
522520
high_voltage
523521
icalendar
@@ -538,6 +536,7 @@ DEPENDENCIES
538536
pickadate-rails
539537
premailer-rails
540538
pry-byebug
539+
pry-rails
541540
pry-remote
542541
public_activity
543542
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: 3 additions & 6 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: Stay connected with the codebar community! Get regular updates on upcoming events, job opportunities, scholarships, and other learning resources.
105102
events:
106103
upcoming: "Upcoming Events"
107104
donation_platforms:
@@ -567,7 +564,7 @@ en:
567564
l2: Compliance with legal obligations
568565
how_we_share:
569566
title: 1.2 How we share your information
570-
p1_html: 'When you subscribe to an event, we share your name with our host companies as we are required to provide a list with all attendee names when you subscribe to any of our events. We **do not share** your email address or any other personal information.'
567+
p1_html: 'When you subscribe to an event, we share your name with our host companies as we are required to provide a list with all attendee names when you subscribe to any of our events. We **do not share** your email address or any other personal information.'
571568
legal_basis:
572569
title: 1.3 Legal basis for our use of your personal information
573570
p1: As set out in the table above, sometimes the legal basis on which we collect and process your data is because our legitimate interests make the processing necessary, and those legitimate interests are not overridden by your interests or fundamental rights and freedoms. For example, we collect and store your data in order to process event information, and to ensure the efficient running and promotion of workshops.
@@ -599,10 +596,10 @@ en:
599596
p1: We reserve the right to modify or update this Privacy Policy at any time in accordance with this provision. If we make changes to this Privacy Policy, we will post the revised Privacy Policy on the codebar website. Please regularly review https://codebar.io/privacy-policy to check for any updates or changes to our Privacy Policy. The date this Privacy Policy was last revised is identified at the top of this page.
600597
cookies:
601598
title: 7. Cookies
602-
p1_html: 'We use cookies to recognise you and your location. You can control cookies through your browser settings and other tools. For more information read our [cookie policy](https://codebar.io/cookie-policy).'
599+
p1_html: 'We use cookies to recognise you and your location. You can control cookies through your browser settings and other tools. For more information read our [cookie policy](https://codebar.io/cookie-policy).'
603600
contact:
604601
title: 8. Contact
605-
p1: "If you have any questions or complaints about this Privacy Policy or our information handling practices, you may email us at [email protected] stating 'Data inquiry' in the subject title, or by postal mail at: codebar Ltd, International House, 101 King's Cross Road, London, WC1X 9LP."
602+
p1: "If you have any questions or complaints about this Privacy Policy or our information handling practices, you may email us at [email protected] stating 'Data inquiry' in the subject title, or by postal mail at: codebar Ltd, International House, 101 King's Cross Road, London, WC1X 9LP."
606603
breach:
607604
title: What happens if you violate codebar’s Code of Conduct?
608605
opening_para: All codebar events are dedicated to providing a harassment-free experience for everyone, regardless of gender, sexual orientation, disability, physical appearance, body size, race, or religion. We do not tolerate harassment towards any community member or organiser in any form. Harassment includes any type of aggressive behaviour or offensive verbal comments related to gender, sexual orientation, disability, physical appearance, body size, race, religion, sexual images in public spaces, deliberate intimidation, stalking, following, harassing photography or recording, sustained disruption of talks or other events, inappropriate physical contact, and unwelcome sexual attention.

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)