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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +12 -0
  3. data/README.md +65 -0
  4. data/Rakefile +21 -0
  5. data/app/assets/config/spree_klaviyo_manifest.js +1 -0
  6. data/app/assets/images/integration_icons/klaviyo-logo.png +0 -0
  7. data/app/clients/spree_klaviyo/klaviyo/client.rb +77 -0
  8. data/app/controllers/spree_klaviyo/addresses_controller_decorator.rb +18 -0
  9. data/app/controllers/spree_klaviyo/profile_controller_decorator.rb +18 -0
  10. data/app/controllers/spree_klaviyo/user_registrations_controller_decorator.rb +22 -0
  11. data/app/jobs/spree_klaviyo/base_job.rb +5 -0
  12. data/app/jobs/spree_klaviyo/create_back_in_stock_subscription_job.rb +10 -0
  13. data/app/jobs/spree_klaviyo/create_event_job.rb +10 -0
  14. data/app/jobs/spree_klaviyo/create_or_update_profile_job.rb +10 -0
  15. data/app/jobs/spree_klaviyo/fetch_profile_job.rb +15 -0
  16. data/app/jobs/spree_klaviyo/subscribe_job.rb +10 -0
  17. data/app/jobs/spree_klaviyo/unsubscribe_job.rb +10 -0
  18. data/app/models/concerns/spree_klaviyo/user_methods.rb +24 -0
  19. data/app/models/spree/integrations/klaviyo.rb +122 -0
  20. data/app/models/spree_klaviyo/analytics_event_handler.rb +61 -0
  21. data/app/models/spree_klaviyo/order_decorator.rb +34 -0
  22. data/app/models/spree_klaviyo/shipment_handler_decorator.rb +25 -0
  23. data/app/models/spree_klaviyo/user_decorator.rb +9 -0
  24. data/app/presenters/spree_klaviyo/address_presenter.rb +27 -0
  25. data/app/presenters/spree_klaviyo/back_in_stock_subscription_presenter.rb +39 -0
  26. data/app/presenters/spree_klaviyo/event_presenter.rb +97 -0
  27. data/app/presenters/spree_klaviyo/line_item_presenter.rb +28 -0
  28. data/app/presenters/spree_klaviyo/order_attributes_presenter.rb +28 -0
  29. data/app/presenters/spree_klaviyo/order_presenter.rb +95 -0
  30. data/app/presenters/spree_klaviyo/product_presenter.rb +32 -0
  31. data/app/presenters/spree_klaviyo/shipment_presenter.rb +53 -0
  32. data/app/presenters/spree_klaviyo/subscribe_presenter.rb +48 -0
  33. data/app/presenters/spree_klaviyo/taxon_presenter.rb +19 -0
  34. data/app/presenters/spree_klaviyo/user_presenter.rb +39 -0
  35. data/app/services/spree_klaviyo/base.rb +5 -0
  36. data/app/services/spree_klaviyo/create_event.rb +16 -0
  37. data/app/services/spree_klaviyo/create_or_update_profile.rb +25 -0
  38. data/app/services/spree_klaviyo/fetch_profile.rb +15 -0
  39. data/app/services/spree_klaviyo/subscribe.rb +13 -0
  40. data/app/services/spree_klaviyo/unsubscribe.rb +13 -0
  41. data/app/views/spree/admin/integrations/forms/_klaviyo.html.erb +48 -0
  42. data/config/i18n-tasks.yml +173 -0
  43. data/config/initializers/spree.rb +4 -0
  44. data/config/locales/en.yml +11 -0
  45. data/config/routes.rb +3 -0
  46. data/lib/generators/spree_klaviyo/install/install_generator.rb +20 -0
  47. data/lib/spree_klaviyo/configuration.rb +8 -0
  48. data/lib/spree_klaviyo/engine.rb +28 -0
  49. data/lib/spree_klaviyo/factories.rb +3 -0
  50. data/lib/spree_klaviyo/testing_support/factories/klaviyo_integration.rb +9 -0
  51. data/lib/spree_klaviyo/version.rb +7 -0
  52. data/lib/spree_klaviyo.rb +11 -0
  53. 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
@@ -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,5 @@
1
+ module SpreeKlaviyo
2
+ class BaseJob < ApplicationJob
3
+ queue_as SpreeKlaviyo.queue
4
+ end
5
+ end
@@ -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,9 @@
1
+ module SpreeKlaviyo
2
+ module UserDecorator
3
+ def self.prepended(base)
4
+ base.include ::SpreeKlaviyo::UserMethods
5
+ end
6
+ end
7
+ end
8
+
9
+ Spree.user_class.prepend(SpreeKlaviyo::UserDecorator) if Spree.user_class.present?
@@ -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