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
@@ -0,0 +1,39 @@
1
+ module SpreeKlaviyo
2
+ class BackInStockSubscriptionPresenter
3
+ def initialize(email:, variant_id:)
4
+ @email = email
5
+ @variant_id = variant_id
6
+ end
7
+
8
+ def call
9
+ {
10
+ data: {
11
+ type: 'back-in-stock-subscription',
12
+ attributes: {
13
+ channels: %w[EMAIL],
14
+ profile: {
15
+ data: {
16
+ type: 'profile',
17
+ attributes: {
18
+ email: email
19
+ }
20
+ }
21
+ }
22
+ },
23
+ relationships: {
24
+ variant: {
25
+ data: {
26
+ type: 'catalog-variant',
27
+ id: "$custom:::$default:::#{variant_id}"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :email, :variant_id
38
+ end
39
+ end
@@ -0,0 +1,97 @@
1
+ module SpreeKlaviyo
2
+ class EventPresenter
3
+ def initialize(integration:, event:, resource:, email:, guest_id: nil)
4
+ @integration = integration
5
+ @store = integration.store
6
+ @resource = resource
7
+ @event = event
8
+ @email = email
9
+ @guest_id = guest_id
10
+ end
11
+
12
+ def call
13
+ {
14
+ data: {
15
+ type: 'event',
16
+ attributes: {
17
+ properties: properties,
18
+ metric: {
19
+ data: {
20
+ type: 'metric',
21
+ attributes: {
22
+ name: @event
23
+ }
24
+ }
25
+ },
26
+ profile: @email.present? ? profile : guest_profile,
27
+ **top_level_attributes
28
+ }
29
+ }
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def top_level_attributes
36
+ if @resource.is_a?(::Spree::Order)
37
+ OrderAttributesPresenter.new(event_name: @event, order: @resource).call
38
+ else
39
+ {}
40
+ end
41
+ end
42
+
43
+ def properties
44
+ if @resource.is_a? ::Spree::Order
45
+ OrderPresenter.new(order: @resource).call
46
+ elsif @resource.is_a? ::Spree::Shipment
47
+ ShipmentPresenter.new(shipment: @resource, store: @store).call
48
+ elsif @resource.is_a? ::Spree::Product
49
+ ProductPresenter.new(product: @resource, store: @store).call
50
+ elsif @resource.is_a? ::Spree::Taxon
51
+ TaxonPresenter.new(taxon: @resource).call
52
+ elsif @resource.is_a? String
53
+ {
54
+ query: @resource
55
+ }
56
+ else
57
+ {}
58
+ end
59
+ end
60
+
61
+ def profile
62
+ if @resource.is_a?(::Spree::Order) && events_that_update_profile.include?(@event)
63
+ UserPresenter.new(
64
+ email: @email,
65
+ address: @resource&.bill_address || @resource&.ship_address,
66
+ user: @resource&.user,
67
+ guest_id: @guest_id
68
+ ).call
69
+ else
70
+ {
71
+ data: {
72
+ type: 'profile',
73
+ attributes: {
74
+ anonymous_id: @guest_id,
75
+ email: @email
76
+ }
77
+ }
78
+ }
79
+ end
80
+ end
81
+
82
+ def guest_profile
83
+ {
84
+ data: {
85
+ type: 'profile',
86
+ attributes: {
87
+ anonymous_id: @guest_id
88
+ }
89
+ }
90
+ }
91
+ end
92
+
93
+ def events_that_update_profile
94
+ @events_that_update_profile ||= [::Spree::Analytics.events[:checkout_email_entered], ::Spree::Analytics.events[:order_completed]]
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,28 @@
1
+ module SpreeKlaviyo
2
+ class LineItemPresenter < ProductPresenter
3
+ include ::Spree::BaseHelper
4
+ include ::Spree::ProductsHelper
5
+
6
+ def initialize(resource:, quantity:, total_price:, currency:, store:, position: nil)
7
+ @resource = resource
8
+ @position = position
9
+ @quantity = quantity
10
+ @total_price = total_price
11
+ super(product: resource&.product, store: store)
12
+ end
13
+
14
+ def call
15
+ return {} if @resource.nil?
16
+
17
+ super.merge({
18
+ quantity: @quantity,
19
+ item_price: @resource.price,
20
+ row_total: @total_price
21
+ })
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :resource, :quantity, :total_price, :currency, :position, :current_store, :feeable_item_ids
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ module SpreeKlaviyo
2
+ class OrderAttributesPresenter
3
+ def initialize(order:, event_name:)
4
+ @order = order
5
+ @event_name = event_name
6
+ end
7
+
8
+ def call
9
+ {
10
+ value: @order.total&.to_f,
11
+ time: time(@order)&.iso8601
12
+ }
13
+ end
14
+
15
+ private
16
+
17
+ def time(order)
18
+ case @event_name
19
+ when ::Spree::Analytics.events[:order_completed]
20
+ order.completed_at
21
+ when 'Order Cancelled'
22
+ order.canceled_at
23
+ else
24
+ order.updated_at
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,95 @@
1
+ module SpreeKlaviyo
2
+ class OrderPresenter
3
+ include ::Spree::BaseHelper
4
+ include ::Spree::ImagesHelper
5
+
6
+ def initialize(order:)
7
+ @order = order
8
+ @current_store = order.store
9
+ @products = products(order)
10
+ end
11
+
12
+ def call
13
+ {
14
+ email: @order&.user&.email || @order.email,
15
+ customer_name: @order.name || ::Spree.t('customer'),
16
+ store_name: @current_store.name,
17
+ order_id: @order.number,
18
+ order_number: @order.number,
19
+ affiliation: @order.store.name,
20
+ value: subtotal,
21
+ subtotal: subtotal,
22
+ item_total: @order.item_total&.to_f,
23
+ total: @order.total.to_f,
24
+ revenue: @order.total&.to_f,
25
+ shipping: @order.shipments.sum(&:cost).to_f,
26
+ shipping_method: shipping_method_names,
27
+ tax: @order.additional_tax_total&.to_f,
28
+ included_tax: @order.included_tax_total&.to_f,
29
+ discount: @order.promo_total&.to_f,
30
+ items: products(@order),
31
+ coupon: @order.coupon_code.to_s,
32
+ currency: @order.currency,
33
+ completed_at: @order.completed_at&.iso8601.to_s,
34
+ checkout_url: ::Spree::Core::Engine.routes.url_helpers.checkout_url(host: @current_store.url_or_custom_domain, token: @order.token),
35
+ all_adjustments: all_adjustments,
36
+ bill_address: AddressPresenter.new(address: @order.bill_address).call,
37
+ ship_address: AddressPresenter.new(address: @order.ship_address).call
38
+ }.merge(try_shipped_shipments)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :order, :current_store
44
+
45
+ def products(order)
46
+ @products ||= order.line_items.includes(variant: { product: :taxons }).map.with_index do |line_item, index|
47
+ LineItemPresenter.new(
48
+ resource: line_item,
49
+ quantity: line_item.quantity,
50
+ total_price: line_item.amount,
51
+ currency: order.currency,
52
+ position: index + 1,
53
+ store: order.store
54
+ ).call
55
+ end
56
+ end
57
+
58
+ def all_adjustments
59
+ @order.all_adjustments.promotion.eligible.group_by(&:label).map do |label, adjustments|
60
+ {
61
+ label: label,
62
+ amount: adjustments.sum(&:amount).to_f
63
+ }
64
+ end
65
+ end
66
+
67
+ def shipping_method_names
68
+ @shipping_method_names ||= @order.shipments.map { |shipment| shipping_method_name(shipment) }.compact.uniq.join(',')
69
+ end
70
+
71
+ def shipping_method_name(shipment)
72
+ shipment.shipping_method&.name.to_s
73
+ end
74
+
75
+ def try_shipped_shipments
76
+ return {} unless @order.fully_shipped? && @order.shipments.shipped.any?
77
+
78
+ {
79
+ shipped_shipments: @order.shipments.shipped.map do |shipment|
80
+ ShipmentPresenter.new(order: @order, shipment: shipment).call
81
+ end
82
+ }
83
+ end
84
+
85
+ def subtotal
86
+ @order.analytics_subtotal
87
+ end
88
+
89
+ def try_variants(variant)
90
+ {
91
+ variant_dict: ::Spree::Variants::OptionsPresenter.new(variant).to_hash
92
+ }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,32 @@
1
+ module SpreeKlaviyo
2
+ class ProductPresenter
3
+ include ::Spree::BaseHelper
4
+ include ::Spree::ImagesHelper
5
+ include Rails.application.routes.mounted_helpers
6
+
7
+ def initialize(product:, store:)
8
+ @product = product
9
+ @current_store = store
10
+ @current_currency = store.default_currency
11
+ end
12
+
13
+ def call
14
+ return {} if @product.nil?
15
+
16
+ {
17
+ name: @product.name,
18
+ price: @product.amount_in(current_currency)&.to_f,
19
+ brand: @product&.brand_name,
20
+ category: @product.main_taxon&.pretty_name,
21
+ currency: current_currency,
22
+ url: spree_storefront_resource_url(@product, store: @store),
23
+ image_url: @product.default_image.present? ? spree_image_url(@product.default_image, width: 1200, height: 1200) : '',
24
+ sku: @product.sku
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :current_store, :current_currency
31
+ end
32
+ end
@@ -0,0 +1,53 @@
1
+ module SpreeKlaviyo
2
+ class ShipmentPresenter < OrderPresenter
3
+ include ::Spree::BaseHelper
4
+ include ::Spree::ProductsHelper
5
+ include ::Spree::ImagesHelper
6
+ include Rails.application.routes.mounted_helpers
7
+
8
+ def initialize(shipment:, order: nil, store: nil)
9
+ @shipment = shipment
10
+ @order = order || shipment.order
11
+ @current_store = store || order.store || shipment.store
12
+ end
13
+
14
+ def call
15
+ {
16
+ customer_name: @order.name || ::Spree.t('customer'),
17
+ email: @order&.user&.email || @order.email,
18
+ order_number: @order.number,
19
+ shipping_method: shipping_method_name(@shipment),
20
+ tracking: @shipment&.tracking.to_s,
21
+ tracking_url: @shipment&.tracking_url,
22
+ store_name: @current_store.name,
23
+ cost: @shipment.final_price.to_f,
24
+ completed_at: @order.completed_at&.iso8601.to_s,
25
+ shipped_items: shipped_items,
26
+ bill_address: AddressPresenter.new(address: @order.bill_address).call,
27
+ ship_address: AddressPresenter.new(address: @order.ship_address).call
28
+ }
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :order, :shipment, :current_store
34
+
35
+ def shipped_items
36
+ @shipment.manifest.map do |shipped_item|
37
+ shipped_items_quantity = shipped_item.line_item.quantity
38
+ {
39
+ url: spree_storefront_resource_url(shipped_item.variant.product),
40
+ image_url: shipped_item.variant.default_image.present? ? spree_image_url(shipped_item.variant.default_image, width: 1200, height: 1200) : '',
41
+ name: shipped_item.variant.name,
42
+ variant: shipped_item.variant.options_text,
43
+ sku: shipped_item.variant.sku,
44
+ shipped_quantity: shipped_item.quantity,
45
+ total_quantity: shipped_items_quantity,
46
+ price: shipped_item.line_item.price.to_f,
47
+ total_price: shipped_item.line_item.amount.to_f,
48
+ brand: brand_name(shipped_item.variant.product)
49
+ }.merge(try_variants(shipped_item.variant))
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ module SpreeKlaviyo
2
+ class SubscribePresenter
3
+ SUBSCRIBED = 'SUBSCRIBED'.freeze
4
+ UNSUBSCRIBED = 'UNSUBSCRIBED'.freeze
5
+
6
+ def initialize(email:, list_id:, type: 'profile-subscription-bulk-create-job', subscribed: true)
7
+ @email = email
8
+ @list_id = list_id
9
+ @type = type
10
+ @subscribed = subscribed
11
+ end
12
+
13
+ def call
14
+ {
15
+ data: {
16
+ type: @type,
17
+ attributes: {
18
+ profiles: {
19
+ data: [
20
+ {
21
+ "type": 'profile',
22
+ "attributes": {
23
+ email: @email,
24
+ "subscriptions": {
25
+ "email": {
26
+ "marketing": {
27
+ "consent": @subscribed ? SUBSCRIBED : UNSUBSCRIBED
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ]
34
+ }
35
+ },
36
+ relationships: {
37
+ list: {
38
+ data: {
39
+ type: 'list',
40
+ id: @list_id
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ module SpreeKlaviyo
2
+ class TaxonPresenter
3
+ include ::Spree::BaseHelper
4
+ include ::Spree::ImagesHelper
5
+ include Rails.application.routes.mounted_helpers
6
+
7
+ def initialize(taxon:)
8
+ @taxon = taxon
9
+ end
10
+
11
+ def call
12
+ {
13
+ name: @taxon.pretty_name,
14
+ image_url: @taxon.image.present? ? spree_image_url(@taxon.image, width: 1200, height: 1200) : '',
15
+ url: spree_storefront_resource_url(@taxon, store: @taxon.store)
16
+ }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ module SpreeKlaviyo
2
+ class UserPresenter
3
+ def initialize(email:, address: nil, user: nil, guest_id: nil)
4
+ @email = email
5
+ @address = address
6
+ @user = user
7
+ @guest_id = guest_id
8
+ end
9
+
10
+ def call
11
+ {
12
+ data: {
13
+ type: 'profile',
14
+ attributes: {
15
+ anonymous_id: @guest_id,
16
+ email: @user.present? ? @user.email : @email,
17
+ first_name: @user&.first_name || @address&.first_name,
18
+ last_name: @user&.last_name || @address&.last_name,
19
+ external_id: @user&.id,
20
+ location: {
21
+ address1: @address&.address1,
22
+ address2: @address&.address2,
23
+ city: @address&.city,
24
+ country: @address&.country_name,
25
+ region: @address&.state_text,
26
+ zip: @address&.zipcode
27
+ }
28
+ }
29
+ }.merge(try_klaviyo_id)
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def try_klaviyo_id
36
+ @user&.klaviyo_id.present? ? { id: @user.klaviyo_id } : {}
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ module SpreeKlaviyo
2
+ class Base
3
+ prepend ::Spree::ServiceModule::Base
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module SpreeKlaviyo
2
+ class CreateEvent < Base
3
+ prepend ::Spree::ServiceModule::Base
4
+
5
+ def call(klaviyo_integration:, event:, resource:, email:, guest_id: nil)
6
+ return failure(false, ::Spree.t('admin.integrations.klaviyo.not_found')) unless klaviyo_integration
7
+
8
+ klaviyo_integration.create_event(
9
+ event: event,
10
+ resource: resource,
11
+ email: email,
12
+ guest_id: guest_id
13
+ )
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ module SpreeKlaviyo
2
+ class CreateOrUpdateProfile < Base
3
+ prepend ::Spree::ServiceModule::Base
4
+
5
+ def call(klaviyo_integration:, user:, guest_id: nil)
6
+ return failure(false, ::Spree.t('admin.integrations.klaviyo.not_found')) unless klaviyo_integration
7
+
8
+ if user.klaviyo_id.blank?
9
+ fetch_profile_result = FetchProfile.call(klaviyo_integration: klaviyo_integration, user: user)
10
+
11
+ if fetch_profile_result.success?
12
+ return klaviyo_integration.update_profile(user, guest_id) if guest_id.present?
13
+
14
+ return fetch_profile_result
15
+ end
16
+
17
+ klaviyo_integration.create_profile(user).tap do |result|
18
+ user.update!(klaviyo_id: JSON.parse(result.value).dig('data', 'id')) if result.success?
19
+ end
20
+ end
21
+
22
+ klaviyo_integration.update_profile(user)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeKlaviyo
2
+ class FetchProfile < Base
3
+ prepend ::Spree::ServiceModule::Base
4
+
5
+ def call(klaviyo_integration:, user:)
6
+ return failure(false, ::Spree.t('admin.integrations.klaviyo.not_found')) unless klaviyo_integration
7
+
8
+ return success(user.klaviyo_id) if user.klaviyo_id.present?
9
+
10
+ klaviyo_integration.fetch_profile(email: user.email).tap do |result|
11
+ user.update!(klaviyo_id: JSON.parse(result.value)['data'].first['id']) if result.success?
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeKlaviyo
2
+ class Subscribe < Base
3
+ prepend ::Spree::ServiceModule::Base
4
+
5
+ def call(klaviyo_integration:, email:, user: nil)
6
+ return failure(false, ::Spree.t('admin.integrations.klaviyo.not_found')) unless klaviyo_integration
7
+
8
+ klaviyo_integration.subscribe_user(email).tap do |result|
9
+ user.update(klaviyo_subscribed: true) if result.success? && user && !user.klaviyo_subscribed?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeKlaviyo
2
+ class Unsubscribe < Base
3
+ prepend ::Spree::ServiceModule::Base
4
+
5
+ def call(klaviyo_integration:, email:, user: nil)
6
+ return failure(false, ::Spree.t('admin.integrations.klaviyo.not_found')) unless klaviyo_integration
7
+
8
+ klaviyo_integration.unsubscribe_user(email).tap do |result|
9
+ user.update(klaviyo_subscribed: false) if result.success? && user && user.klaviyo_subscribed?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ <div class="alert alert-info">
2
+ <div>
3
+ Having trouble? Follow <%= external_link_to " our setup guide", "https://spreecommerce.org/docs/integrations/marketing/klaviyo" %> for more information.
4
+ </div>
5
+ </div>
6
+
7
+ <div class="col-12 col-md-6 p-0">
8
+ <div class="card mb-4">
9
+ <div class="card-body">
10
+ <%= preference_field(
11
+ @integration,
12
+ form,
13
+ "klaviyo_public_api_key",
14
+ i18n_scope: "admin.integrations.klaviyo",
15
+ ) %>
16
+ <p class="mb-4 mt-n2">
17
+ <%= external_link_to "Where to find my API Key?",
18
+ "https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys" %>
19
+ </p>
20
+
21
+ <%= preference_field(
22
+ @integration,
23
+ form,
24
+ "klaviyo_private_api_key",
25
+ i18n_scope: "admin.integrations.klaviyo",
26
+ ) %>
27
+ <p class="mb-4 mt-n2">
28
+ <%= external_link_to "Where to find my Private API key?",
29
+ "https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys" %>
30
+ </p>
31
+
32
+ <%= preference_field(
33
+ @integration,
34
+ form,
35
+ "default_newsletter_list_id",
36
+ i18n_scope: "admin.integrations.klaviyo",
37
+ ) %>
38
+ <p class="mt-n2">
39
+ <%= external_link_to "Where to find list ID?",
40
+ "https://help.klaviyo.com/hc/en-us/articles/115005078647-How-to-find-a-list-ID" %>
41
+ </p>
42
+ <div class="alert alert-info mt-n2 mb-0">
43
+ This list will be used as the default newsletter list for your
44
+ customers signing up from storefront and checkout.
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>