Skip to content

Commit

Permalink
Replace Mailchimp with a Flodesk integration
Browse files Browse the repository at this point in the history
This will allow us to reduce emailing costs by a factor of 10.
  • Loading branch information
gnclmorais committed Jan 25, 2025
1 parent 9ef3d58 commit 5795169
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 141 deletions.
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ gem 'tzinfo-data'
gem 'chosen-rails'
gem 'commonmarker'

gem 'gibbon', '~> 3.5.0'
gem 'faraday'

gem 'stripe'

Expand Down Expand Up @@ -84,6 +84,7 @@ group :development, :test do
gem 'fabrication'
gem 'faker'
gem 'launchy'
gem 'pry-rails'
gem 'pry-byebug'
gem 'pry-remote'
gem 'rspec-collection_matchers'
Expand Down
9 changes: 4 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,6 @@ GEM
foreman (0.88.1)
friendly_id (5.5.1)
activerecord (>= 4.0.0)
gibbon (3.5.0)
faraday (>= 1.0)
multi_json (>= 1.11.0)
globalid (1.2.1)
activesupport (>= 6.1)
haml (6.3.0)
Expand Down Expand Up @@ -231,7 +228,6 @@ GEM
mini_portile2 (2.8.8)
minitest (5.25.1)
msgpack (1.7.2)
multi_json (1.15.0)
multi_xml (0.6.0)
net-http (0.4.1)
uri
Expand Down Expand Up @@ -294,6 +290,8 @@ GEM
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
pry-rails (0.3.11)
pry (>= 0.13.0)
pry-remote (0.1.8)
pry (~> 0.9)
slop (~> 3.0)
Expand Down Expand Up @@ -514,10 +512,10 @@ DEPENDENCIES
dotenv-rails
fabrication
faker
faraday
font_awesome5_rails
foreman
friendly_id
gibbon (~> 3.5.0)
haml
high_voltage
icalendar
Expand All @@ -538,6 +536,7 @@ DEPENDENCIES
pickadate-rails
premailer-rails
pry-byebug
pry-rails
pry-remote
public_activity
puma (~> 6.4)
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/contacts/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
%th Sponsor
%th Contact name
%th Contact email
%th Mailchimp
%th Mailing list
%tbody
- @contacts.each do |contact|
%tr
Expand Down
59 changes: 29 additions & 30 deletions app/views/shared/_newsletter.html.haml
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
- content_for :head do
:javascript
(function(w, d, t, h, s, n) {
w.FlodeskObject = n;
var fn = function() {
(w[n].q = w[n].q || []).push(arguments);
};
w[n] = w[n] || fn;
var f = d.getElementsByTagName(t)[0];
var v = '?v=' + Math.floor(new Date().getTime() / (120 * 1000)) * 60;
var sm = d.createElement(t);
sm.async = true;
sm.type = 'module';
sm.src = h + s + '.mjs' + v;
f.parentNode.insertBefore(sm, f);
var sn = d.createElement(t);
sn.async = true;
sn.noModule = true;
sn.src = h + s + '.js' + v;
f.parentNode.insertBefore(sn, f);
})(window, document, 'script', 'https://assets.flodesk.com', '/universal', 'fd');

.row.justify-content-md-center
.col-md-8
%h2= t('homepage.newsletter.title')
%p= t('homepage.newsletter.description')

-# Mailchimp Signup Form
#mc_embed_signup
%form.validate#mc-embedded-subscribe-form{ action: "https://codebar.us8.list-manage.com/subscribe/post?u=b4652d85b385945c79f2ffa2e&amp;id=3798974cb3",
method: "post",
name: "mc-embedded-subscribe-form",
target: "_blank",
novalidate: true }
#mc_embed_signup_scroll
#indicates-required
.mc-field-group.mb-3
.d-flex.justify-content-between
%label.form-label{ for: "mce-EMAIL" }
Email Address
%span.asterisk *
%small
%span.asterisk * indicates required
%input.required.email.form-control#mce-EMAIL{ type: 'email', value: '', name: 'EMAIL' }
.clear#mce-responses
.response#mce-error-response{ style: 'display:none' }
.response#mce-success-response{ style: 'display:none' }
-# real people should not fill this in and expect good things - do not remove this or risk form bot signups
%div{ style: 'position: absolute; left: -5000px;', 'aria-hidden': 'true' }
%input{ type: 'text', name: 'b_b4652d85b385945c79f2ffa2e_3798974cb3', tabindex: '-1', value: '' }
.d-flex.justify-content-between.align-items-center
%input.btn.btn-primary#mc-embedded-subscribe{ type: 'submit', value: 'Subscribe', name: 'subscribe' }
-# Flodesk signup form
#fd-form-678b693a7ae9331608185173
:javascript
if (window.fd) window.fd('form', {
formId: '678b693a7ae9331608185173',
containerEl: '#fd-form-678b693a7ae9331608185173'
});
9 changes: 9 additions & 0 deletions config/initializers/flodesk.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'services/flodesk'

key = Rails.env.test? ? 'test' : ENV['FLODESK_KEY']

logger.warn 'Missing key for Flodesk' unless key

Flodesk::Client.api_key = key
Flodesk::Client.complete_timeout = 15
Flodesk::Client.open_timeout = 15
6 changes: 0 additions & 6 deletions config/initializers/mailchimp.rb

This file was deleted.

9 changes: 3 additions & 6 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,6 @@ en:
workshop_email_subject: "Regarding hosting a workshop"
chapters:
title: "Chapters"
newsletter:
title: Subscribe to our newsletter
description: Stay connected with the codebar community! Get regular updates on upcoming events, job opportunities, scholarships, and other learning resources.
events:
upcoming: "Upcoming Events"
donation_platforms:
Expand Down Expand Up @@ -567,7 +564,7 @@ en:
l2: Compliance with legal obligations
how_we_share:
title: 1.2 How we share your information
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.'
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.'
legal_basis:
title: 1.3 Legal basis for our use of your personal information
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.
Expand Down Expand Up @@ -599,10 +596,10 @@ en:
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.
cookies:
title: 7. Cookies
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).'
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).'
contact:
title: 8. Contact
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."
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."
breach:
title: What happens if you violate codebar’s Code of Conduct?
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.
Expand Down
157 changes: 157 additions & 0 deletions lib/services/flodesk.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
require 'faraday'

module Flodesk
# Subscriber status
ACTIVE = 'active'.freeze

class Client
API_ENDPOINT = 'https://api.flodesk.com/v1/'.freeze
DEFAULT_TIMEOUT = 60

class_attribute :api_key
class_attribute :complete_timeout
class_attribute :open_timeout

# Allows for setting these values in `config/initializers/flodesk.rb`
class << self
def api_key
@@api_key
end

def complete_timeout
@@complete_timeout
end

def open_timeout
@@open_timeout
end
end

attr_accessor :api_endpoint, :debug, :logger

# We need 3 actions:
#
# 1. subscribe --> params(list_id, email, first_name, last_name)
# Documentation: https://developers.flodesk.com/#tag/subscriber/operation/createOrUpdateSubscriber
# Endpoint: https://api.flodesk.com/v1/subscribers
#
# 2. unsubscribe --> params(list_id, email)
# Documentation: https://developers.flodesk.com/#tag/subscriber/operation/removeSubscriberFromSegments
# Endpoint: https://api.flodesk.com/v1/subscribers/{id_or_email}/segments
#
# 3. subscribed? --> params(list_id, email)
# Documentation: https://developers.flodesk.com/#tag/subscriber/operation/retrieveSubscriber
# Endpoint: https://api.flodesk.com/v1/subscribers/{id_or_email}

def initialize(api_key: nil, complete_timeout: nil, open_timeout: nil)
@api_key = api_key || self.class.api_key || ENV['FLODESK_KEY']
@api_key = @api_key.strip if @api_key

@complete_timeout = complete_timeout || self.class.complete_timeout || DEFAULT_TIMEOUT
@open_timeout = open_timeout || self.class.open_timeout || DEFAULT_TIMEOUT
end

def disabled?
!@api_key
end

def subscribe(email:, first_name:, last_name:, segment_ids:, double_optin: true)
body = { email:, first_name:, last_name:, segment_ids:, double_optin: }

request(:post, 'subscribers', body)
end

def unsubscribe(email:, segment_ids:)
body = { segment_ids: }

request(:delete, "subscribers/#{email}/segments", body)
end

def subscribed?(email:, segment_ids:)
response = request(:get, "subscribers/#{email}")
body = OpenStruct.new(response[:body])

# If not subscribed, stop here
is_active = body.status.to_s.eql?(ACTIVE)
return false unless is_active

segment_ids.all? do |segment_id|
body.segments.any? { |segment| segment_id.to_s.eql?(segment['id']) }
end
end

private

def connection
options = {
headers: {
user_agent: 'codebar (codebar.io)'
},
request: {
timeout: @complete_timeout,
open_timeout: @open_timeout
}
}

# https://lostisland.github.io/faraday/#/customization/request-options
@connection ||= Faraday.new(url: API_ENDPOINT, **options) do |config|
config.request :json

# Beware: the order of these lines matter. Examples:
# - https://mattbrictson.com/blog/advanced-http-techniques-in-ruby#pitfall-raise_error-and-logger-in-the-wrong-order
# - https://stackoverflow.com/a/67182791/590525
config.response :raise_error
config.response :logger, Rails.logger, headers: true, bodies: true, log_level: :debug
config.response :json

# https://developers.flodesk.com/#section/Authentication/api_key
config.request :authorization, 'Basic', -> { @api_key }
end
end

def request(http_method, endpoint, body = {})
# Faraday's `delete` does not accept body at the time of writing
response = if http_method == :delete
connection.run_request(http_method, endpoint, body, nil)
else
connection.public_send(http_method, endpoint, body)
end

{
status: response.status,
body: JSON.parse(response.body)
}
rescue Faraday::Error => e
FlodeskError.new(e.response_body['message'], {
raw_body: e.response_body,
status_code: e.response_status
})
end
end

# Inspired by https://github.com/amro/gibbon/blob/master/lib/gibbon/mailchimp_error.rb
class FlodeskError < StandardError
attr_reader :status_code, :raw_body

def initialize(message = '', params = {})
@status_code = params[:status_code]
@raw_body = params[:raw_body]

super(message)
end

def to_s
"#{super} #{instance_variables_to_s}"
end

private

def instance_variables_to_s
%i[status_code raw_body].map do |attr|
attr_value = send(attr)

"@#{attr}=#{attr_value.inspect}"
end.join(', ')
end
end
end
Loading

0 comments on commit 5795169

Please sign in to comment.