spree_klaviyo 1.0.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 +7 -0
- data/LICENSE.md +12 -0
- data/README.md +65 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_klaviyo_manifest.js +1 -0
- data/app/assets/images/integration_icons/klaviyo-logo.png +0 -0
- data/app/clients/spree_klaviyo/klaviyo/client.rb +77 -0
- data/app/controllers/spree_klaviyo/addresses_controller_decorator.rb +18 -0
- data/app/controllers/spree_klaviyo/profile_controller_decorator.rb +18 -0
- data/app/controllers/spree_klaviyo/user_registrations_controller_decorator.rb +22 -0
- data/app/jobs/spree_klaviyo/base_job.rb +5 -0
- data/app/jobs/spree_klaviyo/create_back_in_stock_subscription_job.rb +10 -0
- data/app/jobs/spree_klaviyo/create_event_job.rb +10 -0
- data/app/jobs/spree_klaviyo/create_or_update_profile_job.rb +10 -0
- data/app/jobs/spree_klaviyo/fetch_profile_job.rb +15 -0
- data/app/jobs/spree_klaviyo/subscribe_job.rb +10 -0
- data/app/jobs/spree_klaviyo/unsubscribe_job.rb +10 -0
- data/app/models/concerns/spree_klaviyo/user_methods.rb +24 -0
- data/app/models/spree/integrations/klaviyo.rb +122 -0
- data/app/models/spree_klaviyo/analytics_event_handler.rb +61 -0
- data/app/models/spree_klaviyo/order_decorator.rb +34 -0
- data/app/models/spree_klaviyo/shipment_handler_decorator.rb +25 -0
- data/app/models/spree_klaviyo/user_decorator.rb +9 -0
- data/app/presenters/spree_klaviyo/address_presenter.rb +27 -0
- data/app/presenters/spree_klaviyo/back_in_stock_subscription_presenter.rb +39 -0
- data/app/presenters/spree_klaviyo/event_presenter.rb +97 -0
- data/app/presenters/spree_klaviyo/line_item_presenter.rb +28 -0
- data/app/presenters/spree_klaviyo/order_attributes_presenter.rb +28 -0
- data/app/presenters/spree_klaviyo/order_presenter.rb +95 -0
- data/app/presenters/spree_klaviyo/product_presenter.rb +32 -0
- data/app/presenters/spree_klaviyo/shipment_presenter.rb +53 -0
- data/app/presenters/spree_klaviyo/subscribe_presenter.rb +48 -0
- data/app/presenters/spree_klaviyo/taxon_presenter.rb +19 -0
- data/app/presenters/spree_klaviyo/user_presenter.rb +39 -0
- data/app/services/spree_klaviyo/base.rb +5 -0
- data/app/services/spree_klaviyo/create_event.rb +16 -0
- data/app/services/spree_klaviyo/create_or_update_profile.rb +25 -0
- data/app/services/spree_klaviyo/fetch_profile.rb +15 -0
- data/app/services/spree_klaviyo/subscribe.rb +13 -0
- data/app/services/spree_klaviyo/unsubscribe.rb +13 -0
- data/app/views/spree/admin/integrations/forms/_klaviyo.html.erb +48 -0
- data/config/i18n-tasks.yml +173 -0
- data/config/initializers/spree.rb +4 -0
- data/config/locales/en.yml +11 -0
- data/config/routes.rb +3 -0
- data/lib/generators/spree_klaviyo/install/install_generator.rb +20 -0
- data/lib/spree_klaviyo/configuration.rb +8 -0
- data/lib/spree_klaviyo/engine.rb +28 -0
- data/lib/spree_klaviyo/factories.rb +3 -0
- data/lib/spree_klaviyo/testing_support/factories/klaviyo_integration.rb +9 -0
- data/lib/spree_klaviyo/version.rb +7 -0
- data/lib/spree_klaviyo.rb +11 -0
- metadata +234 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ca7abe5ed0437048bead7b0b1b074979d85cb5f247d752e4e5ebd638fadc95a6
|
4
|
+
data.tar.gz: 2989bed6e4a9ee6bb6745eb1630e74e5973bb361c51b772a0115ebdeedc6ac21
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7592104f62d2110332fe4a775f8d6e30445c8db480685225cbea112bd3e1f34df9c2259de0e8de114a95deb510aecd697e7f727424013ec6543e4c42320b655f
|
7
|
+
data.tar.gz: 66efd287a71046e3ef4c677ca20e77c6265b1ea6c3c2bf302a3b3ebfc90215259d48f34dd5add56a5c90de8612dc2268151728581e6b18cccda0c4b4d19166dd
|
data/LICENSE.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Copyright (c) 2025 Vendo Connect Inc.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
5
|
+
|
6
|
+
This program is distributed in the hope that it will be useful,
|
7
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
8
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
9
|
+
GNU Affero General Public License for more details.
|
10
|
+
|
11
|
+
You should have received a copy of the GNU Affero General Public License
|
12
|
+
along with this program. If not, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Spree Klaviyo
|
2
|
+
|
3
|
+
This is an official Klaviyo email marketing extension for [Spree Commerce](https://spreecommerce.org).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
1. Add this extension to your Gemfile with this line:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
bundle add spree_klaviyo
|
11
|
+
```
|
12
|
+
|
13
|
+
2. Run the install generator
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
bundle exec rails g spree_klaviyo:install
|
17
|
+
```
|
18
|
+
|
19
|
+
3. Restart your server
|
20
|
+
|
21
|
+
If your server was running, restart it so that it can find the assets properly.
|
22
|
+
|
23
|
+
## Setup guide
|
24
|
+
|
25
|
+
[Please follow our setup guide](https://spreecommerce.org/docs/integrations/marketing/klaviyo) how to setup Klaviyo with Spree Commerce.
|
26
|
+
|
27
|
+
## Developing
|
28
|
+
|
29
|
+
1. Create a dummy app
|
30
|
+
|
31
|
+
```bash
|
32
|
+
bundle update
|
33
|
+
bundle exec rake test_app
|
34
|
+
```
|
35
|
+
|
36
|
+
2. Add your new code
|
37
|
+
3. Run tests
|
38
|
+
|
39
|
+
```bash
|
40
|
+
bundle exec rspec
|
41
|
+
```
|
42
|
+
|
43
|
+
When testing your applications integration with this extension you may use it's factories.
|
44
|
+
Simply add this require statement to your spec_helper:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'spree_klaviyo/factories'
|
48
|
+
```
|
49
|
+
|
50
|
+
## Releasing a new version
|
51
|
+
|
52
|
+
```shell
|
53
|
+
bundle exec gem bump -p -t
|
54
|
+
bundle exec gem release
|
55
|
+
```
|
56
|
+
|
57
|
+
For more options please see [gem-release README](https://github.com/svenfuchs/gem-release)
|
58
|
+
|
59
|
+
## Contributing
|
60
|
+
|
61
|
+
If you'd like to contribute, please take a look at the
|
62
|
+
[instructions](CONTRIBUTING.md) for installing dependencies and crafting a good
|
63
|
+
pull request.
|
64
|
+
|
65
|
+
Copyright (c) 2025 [Vendo Connect Inc.](https://getvendo.com), released under the AGPL 3.0 license.
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'spree/testing_support/extension_rake'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new
|
8
|
+
|
9
|
+
task :default do
|
10
|
+
if Dir['spec/dummy'].empty?
|
11
|
+
Rake::Task[:test_app].invoke
|
12
|
+
Dir.chdir('../../')
|
13
|
+
end
|
14
|
+
Rake::Task[:spec].invoke
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generates a dummy app for testing'
|
18
|
+
task :test_app do
|
19
|
+
ENV['LIB_NAME'] = 'spree_klaviyo'
|
20
|
+
Rake::Task['extension:test_app'].invoke
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_tree ../images
|
Binary file
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
module Klaviyo
|
3
|
+
class Client
|
4
|
+
class Result < ::Spree::ServiceModule::Result; end
|
5
|
+
|
6
|
+
def initialize(public_api_key:, private_api_key:)
|
7
|
+
@public_api_key = public_api_key
|
8
|
+
@private_api_key = private_api_key
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_request(api_endpoint)
|
12
|
+
request = Net::HTTP::Get.new(url(api_endpoint))
|
13
|
+
request["accept"] = "application/json"
|
14
|
+
request["revision"] = SpreeKlaviyo::Config[:klaviyo_api_revision]
|
15
|
+
request["Authorization"] = "Klaviyo-API-Key #{private_api_key}"
|
16
|
+
|
17
|
+
response = http.request(request)
|
18
|
+
|
19
|
+
if response.is_a?(Net::HTTPSuccess)
|
20
|
+
Result.new(true, response.read_body)
|
21
|
+
else
|
22
|
+
Result.new(false, response.read_body)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def post_request(api_endpoint, body)
|
27
|
+
request = Net::HTTP::Post.new(url(api_endpoint))
|
28
|
+
request["accept"] = "application/json"
|
29
|
+
request["revision"] = SpreeKlaviyo::Config[:klaviyo_api_revision]
|
30
|
+
request["content-type"] = "application/json"
|
31
|
+
request["Authorization"] = "Klaviyo-API-Key #{private_api_key}"
|
32
|
+
request.body = body.to_json
|
33
|
+
|
34
|
+
response = http.request(request)
|
35
|
+
|
36
|
+
if response.is_a?(Net::HTTPSuccess)
|
37
|
+
Result.new(true, response.read_body)
|
38
|
+
else
|
39
|
+
Result.new(false, response.read_body)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def patch_request(api_endpoint, body)
|
44
|
+
request = Net::HTTP::Patch.new(url(api_endpoint))
|
45
|
+
request["accept"] = "application/json"
|
46
|
+
request["revision"] = SpreeKlaviyo::Config[:klaviyo_api_revision]
|
47
|
+
request["content-type"] = "application/json"
|
48
|
+
request["Authorization"] = "Klaviyo-API-Key #{private_api_key}"
|
49
|
+
request.body = body.to_json
|
50
|
+
|
51
|
+
response = http.request(request)
|
52
|
+
|
53
|
+
if response.is_a?(Net::HTTPSuccess)
|
54
|
+
Result.new(true, response.read_body)
|
55
|
+
else
|
56
|
+
Result.new(false, response.read_body)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :public_api_key, :private_api_key
|
63
|
+
|
64
|
+
def url(endpoint = "")
|
65
|
+
@url ||= URI.join(SpreeKlaviyo::Config[:klaviyo_api_url], endpoint)
|
66
|
+
end
|
67
|
+
|
68
|
+
def http
|
69
|
+
@http ||= Net::HTTP.new(url.host, url.port).tap do |net_http_instance|
|
70
|
+
net_http_instance.use_ssl = true
|
71
|
+
net_http_instance.open_timeout = SpreeKlaviyo::Config[:klaviyo_api_open_timeout]
|
72
|
+
net_http_instance.read_timeout = SpreeKlaviyo::Config[:klaviyo_api_read_timeout]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
module AddressesControllerDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.include ::Spree::IntegrationsHelper
|
5
|
+
|
6
|
+
base.after_action :create_or_update_klaviyo_profile, only: %i[create update]
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_or_update_klaviyo_profile
|
10
|
+
return unless store_integration('klaviyo').present?
|
11
|
+
return unless @address.valid?
|
12
|
+
|
13
|
+
try_spree_current_user.create_or_update_klaviyo_profile(klaviyo_integration: store_integration('klaviyo'))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Spree::AddressesController.prepend(SpreeKlaviyo::AddressesControllerDecorator)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
module ProfileControllerDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.before_action :create_klaviyo_profile, only: :update
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def create_klaviyo_profile
|
10
|
+
return unless store_integration('klaviyo').present?
|
11
|
+
return unless @user.valid?
|
12
|
+
|
13
|
+
@user.create_or_update_klaviyo_profile(klaviyo_integration: store_integration('klaviyo'))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Spree::Account::ProfileController.prepend(SpreeKlaviyo::ProfileControllerDecorator)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
module UserRegistrationsControllerDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.include ::Spree::AnalyticsHelper
|
5
|
+
base.after_action :create_or_update_klaviyo_profile, only: :create, if: :try_spree_current_user
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def create_or_update_klaviyo_profile
|
11
|
+
integration = store_integration('klaviyo')
|
12
|
+
return if integration.nil?
|
13
|
+
|
14
|
+
try_spree_current_user.create_or_update_klaviyo_profile(
|
15
|
+
klaviyo_integration: integration,
|
16
|
+
guest_id: visitor_id
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Spree::UserRegistrationsController.prepend(SpreeKlaviyo::UserRegistrationsControllerDecorator) if defined?(Spree::UserRegistrationsController)
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class CreateBackInStockSubscriptionJob < BaseJob
|
3
|
+
def perform(klaviyo_integration_id, email, variant_id)
|
4
|
+
variant = ::Spree::Variant.find(variant_id)
|
5
|
+
klaviyo_integration = ::Spree::Integrations::Klaviyo.find(klaviyo_integration_id)
|
6
|
+
|
7
|
+
klaviyo_integration.create_back_in_stock_subscription(email: email, variant_id: variant.id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class CreateEventJob < BaseJob
|
3
|
+
def perform(klaviyo_integration_id, event, resource_id, resource_type, email, guest_id = nil)
|
4
|
+
resource = resource_type.classify.constantize.find(resource_id)
|
5
|
+
klaviyo_integration = ::Spree::Integration.find(klaviyo_integration_id)
|
6
|
+
|
7
|
+
SpreeKlaviyo::CreateEvent.call(klaviyo_integration: klaviyo_integration, event: event, resource: resource, email: email, guest_id: guest_id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class CreateOrUpdateProfileJob < BaseJob
|
3
|
+
def perform(klaviyo_integration_id, user_id, guest_id)
|
4
|
+
user = ::Spree.user_class.find(user_id)
|
5
|
+
klaviyo_integration = ::Spree::Integrations::Klaviyo.find(klaviyo_integration_id)
|
6
|
+
|
7
|
+
SpreeKlaviyo::CreateOrUpdateProfile.call(klaviyo_integration: klaviyo_integration, user: user, guest_id: guest_id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class FetchProfileJob < BaseJob
|
3
|
+
NoProfileFoundError = Class.new(StandardError)
|
4
|
+
|
5
|
+
def perform(klaviyo_integration_id, user_id)
|
6
|
+
klaviyo_integration = ::Spree::Integrations::Klaviyo.find(klaviyo_integration_id)
|
7
|
+
user = ::Spree.user_class.find(user_id)
|
8
|
+
|
9
|
+
result = SpreeKlaviyo::FetchProfile.call(klaviyo_integration: klaviyo_integration, user: user)
|
10
|
+
|
11
|
+
# Due to race condition let's give Klaviyo some time to create profile
|
12
|
+
raise NoProfileFoundError if result.error == ::Spree::Integrations::Klaviyo::NO_PROFILE_FOUND
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class SubscribeJob < BaseJob
|
3
|
+
def perform(klaviyo_integration_id, email, user_id = nil)
|
4
|
+
user = ::Spree.user_class.find(user_id) if user_id.present?
|
5
|
+
klaviyo_integration = ::Spree::Integrations::Klaviyo.find(klaviyo_integration_id)
|
6
|
+
|
7
|
+
SpreeKlaviyo::Subscribe.call(klaviyo_integration: klaviyo_integration, email: email, user: user)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class UnsubscribeJob < BaseJob
|
3
|
+
def perform(klaviyo_integration_id, email, user_id = nil)
|
4
|
+
user = ::Spree.user_class.find(user_id)
|
5
|
+
klaviyo_integration = ::Spree::Integrations::Klaviyo.find(klaviyo_integration_id)
|
6
|
+
|
7
|
+
SpreeKlaviyo::Unsubscribe.call(klaviyo_integration: klaviyo_integration, email: email, user: user)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
module UserMethods
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
store_accessor :private_metadata, :klaviyo_id
|
7
|
+
store_accessor :private_metadata, :klaviyo_subscribed
|
8
|
+
end
|
9
|
+
|
10
|
+
def klaviyo_subscribed?
|
11
|
+
klaviyo_subscribed.to_b
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_or_update_klaviyo_profile(klaviyo_integration:, guest_id: nil)
|
15
|
+
SpreeKlaviyo::CreateOrUpdateProfileJob.perform_later(klaviyo_integration.id, id, guest_id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch_klaviyo_profile(klaviyo_integration:)
|
19
|
+
return if klaviyo_id.present?
|
20
|
+
|
21
|
+
SpreeKlaviyo::FetchProfileJob.perform_later(klaviyo_integration.id, id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Spree
|
2
|
+
module Integrations
|
3
|
+
class Klaviyo < Spree::Integration
|
4
|
+
class Api::Error < StandardError; end
|
5
|
+
|
6
|
+
NO_PROFILE_FOUND = 'No profile found'.freeze
|
7
|
+
|
8
|
+
preference :klaviyo_public_api_key, :string
|
9
|
+
preference :klaviyo_private_api_key, :password
|
10
|
+
preference :default_newsletter_list_id, :string
|
11
|
+
|
12
|
+
validates :preferred_klaviyo_public_api_key, :preferred_klaviyo_private_api_key, :preferred_default_newsletter_list_id, presence: true
|
13
|
+
|
14
|
+
def self.integration_group
|
15
|
+
'marketing'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.icon_path
|
19
|
+
'integration_icons/klaviyo-logo.png'
|
20
|
+
end
|
21
|
+
|
22
|
+
def can_connect?
|
23
|
+
# There's no method for checking if credentials are valid, but we can figure it out basing on response,
|
24
|
+
# except Public API Key.
|
25
|
+
result = client.get_request("lists/#{preferred_default_newsletter_list_id}")
|
26
|
+
|
27
|
+
# 'Missing or invalid private key.' for invalid private key
|
28
|
+
# 'A list with id #{id} does not exist.' for invalid newsletter list id
|
29
|
+
@connection_error_message = JSON.parse(result.value)['errors'].first['detail'] if result.failure?
|
30
|
+
|
31
|
+
result.success?
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_profile(user, guest_id = nil)
|
35
|
+
user_presenter = ::SpreeKlaviyo::UserPresenter.new(email: user.email, address: user&.bill_address, guest_id: guest_id)
|
36
|
+
result = client.post_request('profiles/', user_presenter.call)
|
37
|
+
|
38
|
+
handle_result(result)
|
39
|
+
end
|
40
|
+
|
41
|
+
def update_profile(user, guest_id = nil)
|
42
|
+
user_presenter = ::SpreeKlaviyo::UserPresenter.new(email: user.email, address: user&.bill_address, user: user, guest_id: guest_id)
|
43
|
+
result = client.patch_request("profiles/#{user.klaviyo_id}/", user_presenter.call)
|
44
|
+
|
45
|
+
handle_result(result)
|
46
|
+
end
|
47
|
+
|
48
|
+
def subscribe_user(email)
|
49
|
+
result = client.post_request(
|
50
|
+
'profile-subscription-bulk-create-jobs/',
|
51
|
+
::SpreeKlaviyo::SubscribePresenter.new(email: email, list_id: preferred_default_newsletter_list_id).call
|
52
|
+
)
|
53
|
+
|
54
|
+
handle_result(result)
|
55
|
+
end
|
56
|
+
|
57
|
+
def unsubscribe_user(email)
|
58
|
+
payload = ::SpreeKlaviyo::SubscribePresenter.new(
|
59
|
+
email: email,
|
60
|
+
list_id: preferred_default_newsletter_list_id,
|
61
|
+
type: 'profile-subscription-bulk-delete-job',
|
62
|
+
subscribed: false
|
63
|
+
).call
|
64
|
+
|
65
|
+
result = client.post_request(
|
66
|
+
'profile-subscription-bulk-delete-jobs/',
|
67
|
+
payload
|
68
|
+
)
|
69
|
+
|
70
|
+
handle_result(result)
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_back_in_stock_subscription(email:, variant_id:)
|
74
|
+
body = ::SpreeKlaviyo::BackInStockSubscriptionPresenter.new(email: email, variant_id: variant_id).call
|
75
|
+
result = client.post_request('back-in-stock-subscriptions/', body)
|
76
|
+
|
77
|
+
handle_result(result)
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_event(event:, resource:, email:, guest_id: nil)
|
81
|
+
result = client.post_request(
|
82
|
+
'events/',
|
83
|
+
::SpreeKlaviyo::EventPresenter.new(
|
84
|
+
integration: self,
|
85
|
+
event: event,
|
86
|
+
resource: resource,
|
87
|
+
email: email,
|
88
|
+
guest_id: guest_id
|
89
|
+
).call
|
90
|
+
)
|
91
|
+
|
92
|
+
handle_result(result)
|
93
|
+
end
|
94
|
+
|
95
|
+
def fetch_profile(email:)
|
96
|
+
result = client.get_request("profiles/?fields[profile]=email&filter=equals(email,'#{CGI.escapeURIComponent(email)}')")
|
97
|
+
|
98
|
+
return Spree::ServiceModule::Result.new(false, email, NO_PROFILE_FOUND) if result.success? && JSON.parse(result.value)['data'].empty?
|
99
|
+
|
100
|
+
handle_result(result)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def client
|
106
|
+
::SpreeKlaviyo::Klaviyo::Client.new(
|
107
|
+
public_api_key: preferred_klaviyo_public_api_key,
|
108
|
+
private_api_key: preferred_klaviyo_private_api_key
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def handle_result(result)
|
113
|
+
if result.success?
|
114
|
+
Spree::ServiceModule::Result.new(true, result.value)
|
115
|
+
else
|
116
|
+
Rails.error.report(Api::Error.new(result.value), context: { integration_id: id }, source: 'spree.klaviyo')
|
117
|
+
Spree::ServiceModule::Result.new(false, result.value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class AnalyticsEventHandler < ::Spree::BaseAnalyticsEventHandler
|
3
|
+
def client
|
4
|
+
@client ||= store.integrations.active.find_by(type: 'Spree::Integrations::Klaviyo')
|
5
|
+
end
|
6
|
+
|
7
|
+
def handle_event(event_name, properties)
|
8
|
+
return if client.blank?
|
9
|
+
|
10
|
+
email = user&.email.presence
|
11
|
+
|
12
|
+
record = case event_name
|
13
|
+
when 'product_viewed'
|
14
|
+
properties[:product]
|
15
|
+
when 'product_list_viewed'
|
16
|
+
properties[:taxon]
|
17
|
+
when 'product_searched'
|
18
|
+
properties[:query]
|
19
|
+
when 'product_added', 'product_removed'
|
20
|
+
email ||= properties[:line_item].order.email
|
21
|
+
properties[:line_item].order
|
22
|
+
when 'payment_info_entered'
|
23
|
+
email ||= properties[:order].email
|
24
|
+
properties[:order]
|
25
|
+
when 'coupon_entered', 'coupon_removed'
|
26
|
+
email ||= properties[:order].email
|
27
|
+
properties[:order]
|
28
|
+
when 'coupon_applied'
|
29
|
+
email ||= properties[:order].email
|
30
|
+
properties[:order]
|
31
|
+
when 'coupon_denied'
|
32
|
+
email ||= properties[:order].email
|
33
|
+
properties[:order]
|
34
|
+
when 'checkout_started'
|
35
|
+
email ||= properties[:order].email
|
36
|
+
properties[:order]
|
37
|
+
when 'checkout_email_entered'
|
38
|
+
email = properties[:email]
|
39
|
+
properties[:order]
|
40
|
+
when 'checkout_step_viewed', 'checkout_step_completed'
|
41
|
+
email ||= properties[:order].email
|
42
|
+
properties[:order]
|
43
|
+
when 'order_completed'
|
44
|
+
email ||= properties[:order].email
|
45
|
+
properties[:order]
|
46
|
+
when 'subscribed_to_newsletter'
|
47
|
+
email ||= properties[:email]
|
48
|
+
SpreeKlaviyo::SubscribeJob.perform_later(client.id, email, user&.id)
|
49
|
+
nil
|
50
|
+
when 'unsubscribed_from_newsletter'
|
51
|
+
email ||= properties[:email]
|
52
|
+
SpreeKlaviyo::UnsubscribeJob.perform_later(client.id, email, user&.id)
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
return if email&.strip&.blank? && identity_hash[:visitor_id].blank?
|
57
|
+
|
58
|
+
client.create_event(event: event_human_name(event_name), resource: record, email: email, guest_id: identity_hash[:visitor_id])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
module OrderDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.state_machine.after_transition to: :complete, do: :subscribe_user_to_klaviyo_newsletter
|
5
|
+
base.state_machine.after_transition to: :canceled, do: :track_order_cancelled_event
|
6
|
+
end
|
7
|
+
|
8
|
+
def subscribe_user_to_klaviyo_newsletter
|
9
|
+
return unless accept_marketing?
|
10
|
+
|
11
|
+
return if user&.klaviyo_subscribed?
|
12
|
+
|
13
|
+
integration = store_integration('klaviyo')
|
14
|
+
return if integration.blank?
|
15
|
+
|
16
|
+
SpreeKlaviyo::SubscribeJob.perform_later(integration.id, email, user_id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def track_order_cancelled_event
|
20
|
+
klaviyo_integration = store.integrations.active.find_by(type: 'Spree::Integrations::Klaviyo')
|
21
|
+
return if klaviyo_integration.nil?
|
22
|
+
|
23
|
+
klaviyo_integration.create_event(event: 'Order Cancelled', resource: self, email: email)
|
24
|
+
rescue StandardError => e
|
25
|
+
Rails.error.report(
|
26
|
+
e,
|
27
|
+
context: { event_name: 'order_cancelled', record: { order: self } },
|
28
|
+
source: 'spree.core'
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
::Spree::Order.prepend(OrderDecorator)
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
module ShipmentHandlerDecorator
|
3
|
+
def perform
|
4
|
+
super
|
5
|
+
track_package_shipped_event
|
6
|
+
end
|
7
|
+
|
8
|
+
def track_package_shipped_event
|
9
|
+
order = @shipment.order
|
10
|
+
|
11
|
+
klaviyo_integration = order.store.integrations.active.find_by(type: 'Spree::Integrations::Klaviyo')
|
12
|
+
return if klaviyo_integration.blank?
|
13
|
+
|
14
|
+
klaviyo_integration.create_event(event: 'Package Shipped', resource: @shipment, email: order.email)
|
15
|
+
rescue StandardError => e
|
16
|
+
Rails.error.report(
|
17
|
+
e,
|
18
|
+
context: { event_name: 'package_shipped', record: { order: order, shipment: @shipment } },
|
19
|
+
source: 'spree.core'
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
::Spree::ShipmentHandler.prepend(ShipmentHandlerDecorator)
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SpreeKlaviyo
|
2
|
+
class AddressPresenter
|
3
|
+
def initialize(address:)
|
4
|
+
@address = address
|
5
|
+
end
|
6
|
+
|
7
|
+
def call
|
8
|
+
return {} if @address.nil?
|
9
|
+
|
10
|
+
{
|
11
|
+
city: @address.city,
|
12
|
+
country: @address.country_name,
|
13
|
+
postalCode: @address.zipcode,
|
14
|
+
state: @address.state_text,
|
15
|
+
street: @address.street,
|
16
|
+
phone: @address.phone,
|
17
|
+
name: @address.full_name,
|
18
|
+
first_name: @address.first_name,
|
19
|
+
last_name: @address.last_name
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :address
|
26
|
+
end
|
27
|
+
end
|