solidus_paypal_braintree 0.4.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +44 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.gitignore +19 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +76 -0
  8. data/CHANGELOG.md +327 -0
  9. data/Gemfile +38 -0
  10. data/LICENSE +2 -2
  11. data/README.md +170 -25
  12. data/Rakefile +4 -25
  13. data/app/assets/config/solidus_paypal_braintree_manifest.js +1 -0
  14. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_blue_button_280x48.svg +19 -0
  15. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_blue_button_320x48.svg +19 -0
  16. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_blue_button_375x48.svg +19 -0
  17. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_white_button_280x48.svg +19 -0
  18. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_white_button_320x48.svg +19 -0
  19. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_white_button_375x48.svg +19 -0
  20. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_acceptance_mark.svg +15 -0
  21. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_button_280x48.svg +19 -0
  22. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_button_320x48.svg +19 -0
  23. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_button_375x48.svg +19 -0
  24. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_logo.svg +18 -0
  25. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_acceptance_mark.svg +20 -0
  26. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_button_280x48.svg +19 -0
  27. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_button_320x48.svg +19 -0
  28. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_button_375x48.svg +19 -0
  29. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_logo.svg +18 -0
  30. data/app/assets/javascripts/solidus_paypal_braintree/checkout.js +32 -3
  31. data/app/assets/javascripts/solidus_paypal_braintree/client.js +57 -4
  32. data/app/assets/javascripts/solidus_paypal_braintree/constants.js +27 -0
  33. data/app/assets/javascripts/solidus_paypal_braintree/frontend.js +2 -0
  34. data/app/assets/javascripts/solidus_paypal_braintree/hosted_form.js +15 -5
  35. data/app/assets/javascripts/solidus_paypal_braintree/paypal_button.js +71 -21
  36. data/app/assets/javascripts/solidus_paypal_braintree/paypal_messaging.js +22 -0
  37. data/app/assets/javascripts/solidus_paypal_braintree/venmo_button.js +86 -0
  38. data/app/assets/javascripts/spree/backend/solidus_paypal_braintree.js +2 -2
  39. data/app/assets/javascripts/spree/frontend/paypal_button.js +3 -3
  40. data/app/assets/stylesheets/spree/frontend/solidus_paypal_braintree.css +12 -0
  41. data/app/decorators/controllers/solidus_paypal_braintree/admin_payments_controller_decorator.rb +11 -0
  42. data/app/decorators/controllers/solidus_paypal_braintree/checkout_controller_decorator.rb +11 -0
  43. data/app/decorators/controllers/solidus_paypal_braintree/client_tokens_controller.rb +41 -0
  44. data/app/decorators/controllers/solidus_paypal_braintree/orders_controller_decorator.rb +11 -0
  45. data/app/decorators/models/solidus_paypal_braintree/spree/store_decorator.rb +20 -0
  46. data/app/decorators/models/solidus_paypal_braintree/spree/user_decorator.rb +13 -0
  47. data/app/helpers/solidus_paypal_braintree/braintree_admin_helper.rb +23 -0
  48. data/app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb +60 -0
  49. data/app/models/application_record.rb +2 -0
  50. data/app/models/solidus_paypal_braintree/address.rb +64 -0
  51. data/app/models/solidus_paypal_braintree/avs_result.rb +3 -0
  52. data/app/models/solidus_paypal_braintree/configuration.rb +39 -3
  53. data/app/models/solidus_paypal_braintree/customer.rb +7 -3
  54. data/app/models/solidus_paypal_braintree/gateway.rb +80 -21
  55. data/app/models/solidus_paypal_braintree/response.rb +3 -2
  56. data/app/models/solidus_paypal_braintree/source.rb +51 -8
  57. data/app/models/solidus_paypal_braintree/transaction.rb +5 -3
  58. data/app/models/solidus_paypal_braintree/transaction_address.rb +34 -12
  59. data/app/models/solidus_paypal_braintree/transaction_import.rb +17 -12
  60. data/app/overrides/spree/payments/payment/add_paypal_funding_source_to_payment.rb +9 -0
  61. data/app/views/spree/api/payments/source_views/_paypal_braintree.json.jbuilder +3 -0
  62. data/app/views/spree/checkout/existing_payment/_paypal_braintree.html.erb +10 -0
  63. data/app/views/spree/shared/_apple_pay_button.html.erb +2 -2
  64. data/app/views/spree/shared/_braintree_errors.html.erb +14 -17
  65. data/app/views/spree/shared/_braintree_hosted_fields.html.erb +24 -9
  66. data/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb +13 -6
  67. data/app/views/spree/shared/_paypal_cart_button.html.erb +18 -2
  68. data/app/views/spree/shared/_paypal_messaging.html.erb +13 -0
  69. data/app/views/spree/shared/_venmo_button.html.erb +33 -0
  70. data/bin/console +17 -0
  71. data/bin/rails +15 -0
  72. data/bin/setup +8 -0
  73. data/config/locales/en.yml +44 -0
  74. data/config/locales/it.yml +53 -8
  75. data/config/routes.rb +2 -0
  76. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +3 -1
  77. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +5 -7
  78. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +3 -1
  79. data/db/migrate/20190705115327_add_paypal_button_preferences_to_braintree_configurations.rb +5 -0
  80. data/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb +5 -0
  81. data/db/migrate/20211222170950_add_paypal_funding_source_to_solidus_paypal_braintree_sources.rb +5 -0
  82. data/db/migrate/20220104150301_add_venmo_to_braintree_configuration.rb +5 -0
  83. data/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +23 -5
  84. data/lib/controllers/frontend/solidus_paypal_braintree/checkouts_controller.rb +25 -21
  85. data/lib/controllers/frontend/solidus_paypal_braintree/transactions_controller.rb +56 -51
  86. data/lib/generators/solidus_paypal_braintree/install/install_generator.rb +7 -5
  87. data/lib/solidus_paypal_braintree/country_mapper.rb +4 -2
  88. data/lib/solidus_paypal_braintree/engine.rb +17 -13
  89. data/lib/solidus_paypal_braintree/factories.rb +39 -4
  90. data/lib/solidus_paypal_braintree/request_protection.rb +3 -0
  91. data/lib/solidus_paypal_braintree/version.rb +3 -1
  92. data/lib/solidus_paypal_braintree.rb +4 -0
  93. data/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +38 -5
  94. data/lib/views/backend/spree/admin/payments/source_forms/_paypal_braintree.html.erb +2 -2
  95. data/lib/views/backend/spree/admin/payments/source_views/_paypal_braintree.html.erb +7 -2
  96. data/lib/views/backend/spree/admin/shared/preference_fields/_preference_select.html.erb +13 -0
  97. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_paypal_braintree.html.erb +2 -2
  98. data/lib/views/frontend/solidus_paypal_braintree/payments/_payment.html.erb +12 -0
  99. data/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +8 -2
  100. data/lib/views/frontend/spree/shared/_paypal_checkout_button.html.erb +32 -0
  101. data/solidus_paypal_braintree.gemspec +42 -0
  102. metadata +95 -188
  103. data/app/controllers/solidus_paypal_braintree/client_tokens_controller.rb +0 -22
  104. data/app/helpers/braintree_admin_helper.rb +0 -18
  105. data/app/models/spree/store_decorator.rb +0 -11
  106. data/app/views/spree/shared/_paypal_checkout_button.html.erb +0 -27
  107. data/config/initializers/braintree.rb +0 -1
@@ -1,22 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'braintree'
2
4
 
3
5
  module SolidusPaypalBraintree
4
6
  class Gateway < ::Spree::PaymentMethod
5
7
  include RequestProtection
6
8
 
9
+ class TokenGenerationDisabledError < StandardError; end
10
+
7
11
  # Error message from Braintree that gets returned by a non voidable transaction
8
- NON_VOIDABLE_STATUS_ERROR_REGEXP = /can only be voided if status is authorized/
12
+ NON_VOIDABLE_STATUS_ERROR_REGEXP = /can only be voided if status is authorized/.freeze
9
13
 
10
14
  TOKEN_GENERATION_DISABLED_MESSAGE = 'Token generation is disabled.' \
11
- ' To re-enable set the `token_generation_enabled` preference on the' \
12
- ' gateway to `true`.'.freeze
15
+ ' To re-enable set the `token_generation_enabled` preference on the' \
16
+ ' gateway to `true`.'
13
17
 
14
18
  ALLOWED_BRAINTREE_OPTIONS = [
15
19
  :device_data,
16
20
  :device_session_id,
17
21
  :merchant_account_id,
18
22
  :order_id
19
- ]
23
+ ].freeze
20
24
 
21
25
  VOIDABLE_STATUSES = [
22
26
  Braintree::Transaction::Status::SubmittedForSettlement,
@@ -35,9 +39,37 @@ module SolidusPaypalBraintree
35
39
  preference(:merchant_id, :string, default: nil)
36
40
  preference(:public_key, :string, default: nil)
37
41
  preference(:private_key, :string, default: nil)
42
+ preference(:http_open_timeout, :integer, default: 60)
43
+ preference(:http_read_timeout, :integer, default: 60)
38
44
  preference(:merchant_currency_map, :hash, default: {})
39
45
  preference(:paypal_payee_email_map, :hash, default: {})
40
46
 
47
+ # Which checkout flow to use (vault/checkout)
48
+ preference(:paypal_flow, :string, default: 'vault')
49
+
50
+ # A hash that gets passed to the `style` key when initializing the credit card fields.
51
+ # See https://developers.braintreepayments.com/guides/hosted-fields/styling/javascript/v3
52
+ preference(:credit_card_fields_style, :hash, default: {})
53
+
54
+ # A hash that gets its keys passed to the associated braintree field placeholder tag.
55
+ # Example: { number: "Enter card number", cvv: "Enter CVV", expirationDate: "mm/yy" }
56
+ preference(:placeholder_text, :hash, default: {})
57
+
58
+ # Wether to use the JS device data collector
59
+ preference(:use_data_collector, :boolean, default: true)
60
+
61
+ # Useful for testing purposes, as PayPal will show funding sources based on the buyer's country;
62
+ # usually retrieved by their ip geolocation. I.e. Venmo will show for US buyers, but not European.
63
+ preference(:force_buyer_country, :string)
64
+
65
+ preference(:enable_venmo_funding, :boolean, default: false)
66
+
67
+ # When on mobile, paying with Venmo, the user may be returned to the same store tab
68
+ # depending on if their browser supports it, otherwise a new tab will be created
69
+ # However, returning to a new tab may break the payment checkout flow for some stores, for example,
70
+ # if they are single-page applications (SPA). Set this to false if this is the case
71
+ preference(:venmo_new_tab_support, :boolean, default: true)
72
+
41
73
  def partial_name
42
74
  "paypal_braintree"
43
75
  end
@@ -57,6 +89,8 @@ module SolidusPaypalBraintree
57
89
  merchant_id: preferred_merchant_id,
58
90
  public_key: preferred_public_key,
59
91
  private_key: preferred_private_key,
92
+ http_open_timeout: preferred_http_open_timeout,
93
+ http_read_timeout: preferred_http_read_timeout,
60
94
  logger: logger
61
95
  }
62
96
  end
@@ -73,7 +107,7 @@ module SolidusPaypalBraintree
73
107
  protected_request do
74
108
  result = braintree.transaction.sale(
75
109
  amount: dollars(money_cents),
76
- **transaction_options(source, gateway_options, true)
110
+ **transaction_options(source, gateway_options, submit_for_settlement: true)
77
111
  )
78
112
 
79
113
  Response.build(result)
@@ -200,7 +234,7 @@ module SolidusPaypalBraintree
200
234
  return if source.token.present? || source.customer.present? || source.nonce.nil?
201
235
 
202
236
  result = braintree.customer.create(customer_profile_params(payment))
203
- fail Spree::Core::GatewayError, result.message unless result.success?
237
+ fail ::Spree::Core::GatewayError, result.message unless result.success?
204
238
 
205
239
  customer = result.customer
206
240
 
@@ -213,12 +247,12 @@ module SolidusPaypalBraintree
213
247
  end
214
248
  end
215
249
 
250
+ # @raise [TokenGenerationDisabledError]
251
+ # If `preferred_token_generation_enabled` is false
252
+ #
216
253
  # @return [String]
217
254
  # The token that should be used along with the Braintree js-client sdk.
218
255
  #
219
- # returns an error message if `preferred_token_generation_enabled` is
220
- # set to false.
221
- #
222
256
  # @example
223
257
  # <script>
224
258
  # var token = #{Spree::Braintree::Gateway.first!.generate_token}
@@ -233,7 +267,10 @@ module SolidusPaypalBraintree
233
267
  # );
234
268
  # </script>
235
269
  def generate_token
236
- return TOKEN_GENERATION_DISABLED_MESSAGE unless preferred_token_generation_enabled
270
+ unless preferred_token_generation_enabled
271
+ raise TokenGenerationDisabledError, TOKEN_GENERATION_DISABLED_MESSAGE
272
+ end
273
+
237
274
  braintree.client_token.generate
238
275
  end
239
276
 
@@ -261,6 +298,12 @@ module SolidusPaypalBraintree
261
298
 
262
299
  private
263
300
 
301
+ # Whether to store this payment method in the PayPal Vault. This only works when the checkout
302
+ # flow is "vault", so make sure to call +super+ if you override it.
303
+ def store_in_vault
304
+ preferred_paypal_flow == 'vault'
305
+ end
306
+
264
307
  def logger
265
308
  Braintree::Configuration.logger.clone.tap do |logger|
266
309
  logger.level = Rails.logger.level
@@ -279,20 +322,24 @@ module SolidusPaypalBraintree
279
322
  JSON.parse(preference_string.gsub("=>", ":"))
280
323
  end
281
324
 
282
- def convert_preference_value(value, type)
325
+ def convert_preference_value(value, type, preference_encryptor = nil)
283
326
  if type == :hash && value.is_a?(String)
284
327
  value = to_hash(value)
285
328
  end
286
- super
329
+ if method(__method__).super_method.arity == 3
330
+ super
331
+ else
332
+ super(value, type)
333
+ end
287
334
  end
288
335
 
289
- def transaction_options(source, options, submit_for_settlement = false)
336
+ def transaction_options(source, options, submit_for_settlement: false)
290
337
  params = options.select do |key, _|
291
338
  ALLOWED_BRAINTREE_OPTIONS.include?(key)
292
339
  end
293
340
 
294
341
  params[:channel] = "Solidus"
295
- params[:options] = { store_in_vault_on_success: true }
342
+ params[:options] = { store_in_vault_on_success: store_in_vault }
296
343
 
297
344
  if submit_for_settlement
298
345
  params[:options][:submit_for_settlement] = true
@@ -302,6 +349,10 @@ module SolidusPaypalBraintree
302
349
  params[:options][:paypal] = { payee_email: paypal_email }
303
350
  end
304
351
 
352
+ if source.venmo? && venmo_business_profile_id
353
+ params[:options][:venmo] = { profile_id: venmo_business_profile_id }
354
+ end
355
+
305
356
  if merchant_account_id = merchant_account_for(source, options)
306
357
  params[:merchant_account_id] = merchant_account_id
307
358
  end
@@ -349,25 +400,33 @@ module SolidusPaypalBraintree
349
400
  end
350
401
 
351
402
  def merchant_account_for(_source, options)
352
- if options[:currency]
353
- preferred_merchant_currency_map[options[:currency]]
354
- end
403
+ return unless options[:currency]
404
+
405
+ preferred_merchant_currency_map[options[:currency]]
355
406
  end
356
407
 
357
408
  def paypal_payee_email_for(source, options)
358
- if source.paypal?
359
- preferred_paypal_payee_email_map[options[:currency]]
360
- end
409
+ return unless source.paypal?
410
+
411
+ preferred_paypal_payee_email_map[options[:currency]]
361
412
  end
362
413
 
363
414
  def customer_profile_params(payment)
364
415
  params = {}
365
416
 
366
- if payment.source.try(:nonce)
417
+ params[:email] = payment&.order&.email
418
+
419
+ if store_in_vault && payment.source.try(:nonce)
367
420
  params[:payment_method_nonce] = payment.source.nonce
368
421
  end
369
422
 
370
423
  params
371
424
  end
425
+
426
+ # override with the Venmo business profile that you want to use for transactions,
427
+ # or leave it to be nil if want Braintree to use your default account
428
+ def venmo_business_profile_id
429
+ nil
430
+ end
372
431
  end
373
432
  end
@@ -27,7 +27,7 @@ module SolidusPaypalBraintree
27
27
  transaction = result.transaction
28
28
  options = response_options(transaction).update(
29
29
  # For error responses we want to have the CVV code
30
- cvv_result: transaction.try!(:cvv_response_code)
30
+ cvv_result: transaction&.cvv_response_code
31
31
  )
32
32
  new(false, error_message(result), result.params, options)
33
33
  end
@@ -35,6 +35,7 @@ module SolidusPaypalBraintree
35
35
  def response_options(transaction)
36
36
  # Some error responses do not have a transaction
37
37
  return {} if transaction.nil?
38
+
38
39
  {
39
40
  authorization: transaction.id,
40
41
  avs_result: SolidusPaypalBraintree::AVSResult.build(transaction),
@@ -67,7 +68,7 @@ module SolidusPaypalBraintree
67
68
  when 'settlement_declined'
68
69
  I18n.t(transaction.processor_settlement_response_code,
69
70
  scope: 'solidus_paypal_braintree.processor_settlement_response_codes',
70
- default: "#{transaction.processor_settlement_response_text} (#{transaction.processor_settlement_response_code})")
71
+ default: "#{transaction.processor_settlement_response_text} (#{transaction.processor_settlement_response_code})") # rubocop:disable Layout/LineLength
71
72
  else
72
73
  I18n.t(transaction.status,
73
74
  scope: 'solidus_paypal_braintree.transaction_statuses',
@@ -1,24 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidusPaypalBraintree
2
4
  class Source < SolidusSupport.payment_source_parent_class
3
5
  include RequestProtection
4
6
 
5
7
  PAYPAL = "PayPalAccount"
6
8
  APPLE_PAY = "ApplePayCard"
9
+ VENMO = "VenmoAccount"
7
10
  CREDIT_CARD = "CreditCard"
8
11
 
9
- belongs_to :user, class_name: Spree::UserClassHandle.new
12
+ enum paypal_funding_source: {
13
+ applepay: 0, bancontact: 1, blik: 2, boleto: 3, card: 4, credit: 5, eps: 6, giropay: 7, ideal: 8,
14
+ itau: 9, maxima: 10, mercadopago: 11, mybank: 12, oxxo: 13, p24: 14, paylater: 15, paypal: 16, payu: 17,
15
+ sepa: 18, sofort: 19, trustly: 20, venmo: 21, verkkopankki: 22, wechatpay: 23, zimpler: 24
16
+ }, _suffix: :funding
17
+
18
+ belongs_to :user, class_name: ::Spree::UserClassHandle.new, optional: true
10
19
  belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
11
- has_many :payments, as: :source, class_name: "Spree::Payment"
20
+ has_many :payments, as: :source, class_name: "Spree::Payment", dependent: :destroy
12
21
 
13
- belongs_to :customer, class_name: "SolidusPaypalBraintree::Customer"
22
+ belongs_to :customer, class_name: "SolidusPaypalBraintree::Customer", optional: true
14
23
 
15
- validates :payment_type, inclusion: [PAYPAL, APPLE_PAY, CREDIT_CARD]
24
+ validates :payment_type, inclusion: [PAYPAL, APPLE_PAY, VENMO, CREDIT_CARD]
25
+
26
+ before_save :clear_paypal_funding_source, unless: :paypal?
16
27
 
17
28
  scope(:with_payment_profile, -> { joins(:customer) })
18
29
  scope(:credit_card, -> { where(payment_type: CREDIT_CARD) })
19
30
 
20
- delegate :last_4, :card_type, to: :braintree_payment_method, allow_nil: true
31
+ delegate :last_4, :card_type, :expiration_month, :expiration_year, :email,
32
+ :username, :source_description, to: :braintree_payment_method, allow_nil: true
33
+
34
+ # Aliases to match Spree::CreditCard's interface
21
35
  alias_method :last_digits, :last_4
36
+ alias_method :month, :expiration_month
37
+ alias_method :year, :expiration_year
38
+ alias_method :cc_type, :card_type
22
39
 
23
40
  # we are not currenctly supporting an "imported" flag
24
41
  def imported
@@ -35,6 +52,7 @@ module SolidusPaypalBraintree
35
52
 
36
53
  def can_void?(payment)
37
54
  return false unless payment.response_code
55
+
38
56
  transaction = protected_request do
39
57
  braintree_client.transaction.find(payment.response_code)
40
58
  end
@@ -59,8 +77,12 @@ module SolidusPaypalBraintree
59
77
  payment_type == PAYPAL
60
78
  end
61
79
 
80
+ def venmo?
81
+ payment_type == VENMO
82
+ end
83
+
62
84
  def reusable?
63
- true
85
+ token.present?
64
86
  end
65
87
 
66
88
  def credit_card?
@@ -68,13 +90,30 @@ module SolidusPaypalBraintree
68
90
  end
69
91
 
70
92
  def display_number
71
- "XXXX-XXXX-XXXX-#{last_digits.to_s.rjust(4, 'X')}"
93
+ if paypal?
94
+ email
95
+ elsif venmo?
96
+ username
97
+ else
98
+ "XXXX-XXXX-XXXX-#{last_digits.to_s.rjust(4, 'X')}"
99
+ end
100
+ end
101
+
102
+ def display_paypal_funding_source
103
+ I18n.t(paypal_funding_source,
104
+ scope: 'solidus_paypal_braintree.paypal_funding_sources',
105
+ default: paypal_funding_source)
106
+ end
107
+
108
+ def display_payment_type
109
+ "#{I18n.t('solidus_paypal_braintree.payment_type.label')}: #{friendly_payment_type}"
72
110
  end
73
111
 
74
112
  private
75
113
 
76
114
  def braintree_payment_method
77
- return unless braintree_client && credit_card?
115
+ return unless braintree_client
116
+
78
117
  @braintree_payment_method ||= protected_request do
79
118
  braintree_client.payment_method.find(token)
80
119
  end
@@ -86,5 +125,9 @@ module SolidusPaypalBraintree
86
125
  def braintree_client
87
126
  @braintree_client ||= payment_method.try(:braintree)
88
127
  end
128
+
129
+ def clear_paypal_funding_source
130
+ self.paypal_funding_source = nil
131
+ end
89
132
  end
90
133
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
 
3
5
  module SolidusPaypalBraintree
4
6
  class Transaction
5
7
  include ActiveModel::Model
6
8
 
7
- attr_accessor :nonce, :payment_method, :payment_type, :address, :email, :phone
9
+ attr_accessor :nonce, :payment_method, :payment_type, :paypal_funding_source, :address, :email, :phone
8
10
 
9
11
  validates :nonce, presence: true
10
12
  validates :payment_method, presence: true
@@ -16,8 +18,8 @@ module SolidusPaypalBraintree
16
18
  errors.add(:payment_method, 'Must be braintree')
17
19
  end
18
20
  if address && !address.valid?
19
- address.errors.each do |field, error|
20
- errors.add(:address, "#{field} #{error}")
21
+ address.errors.each do |error|
22
+ errors.add(:address, error.full_message)
21
23
  end
22
24
  end
23
25
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
 
3
5
  module SolidusPaypalBraintree
@@ -6,11 +8,11 @@ module SolidusPaypalBraintree
6
8
  include ActiveModel::Validations::Callbacks
7
9
  include SolidusPaypalBraintree::CountryMapper
8
10
 
9
- attr_accessor :country_code, :last_name, :first_name,
10
- :city, :zip, :state_code, :address_line_1, :address_line_2
11
+ attr_accessor :country_code, :name, :city, :zip, :state_code,
12
+ :address_line_1, :address_line_2, :first_name, :last_name
11
13
 
12
- validates :first_name, :last_name, :address_line_1, :city, :zip,
13
- :country_code, presence: true
14
+ validates :address_line_1, :city, :zip, :country_code, presence: true
15
+ validates :name, presence: true, unless: ->(address){ address.first_name.present? }
14
16
 
15
17
  before_validation do
16
18
  self.country_code = country_code.presence || "us"
@@ -29,25 +31,45 @@ module SolidusPaypalBraintree
29
31
  end
30
32
 
31
33
  def spree_country
32
- country_code && (@country ||= Spree::Country.find_by(iso: country_code.upcase))
34
+ country_code && (@country ||= ::Spree::Country.find_by(iso: country_code.upcase))
33
35
  end
34
36
 
35
37
  def spree_state
36
- spree_country && state_code && ( @state ||= spree_country.states.where(
37
- Spree::State.arel_table[:name].matches(state_code).or(
38
- Spree::State.arel_table[:abbr].matches(state_code)
38
+ spree_country && state_code && ( @state ||= spree_country.states.find_by(
39
+ ::Spree::State.arel_table[:name].matches(state_code).or(
40
+ ::Spree::State.arel_table[:abbr].matches(state_code)
39
41
  )
40
- ).first )
42
+ ))
41
43
  end
42
44
 
43
45
  def to_spree_address
44
- address = Spree::Address.new first_name: first_name,
45
- last_name: last_name,
46
+ address = ::Spree::Address.new(
46
47
  city: city,
47
48
  country: spree_country,
48
49
  address1: address_line_1,
49
50
  address2: address_line_2,
50
51
  zipcode: zip
52
+ )
53
+
54
+ if SolidusSupport.combined_first_and_last_name_in_address?
55
+ address.name = begin
56
+ if first_name.nil?
57
+ name
58
+ else
59
+ [first_name, last_name].join(" ")
60
+ end
61
+ end
62
+ else
63
+ ::Spree::Deprecation.warn("first_name and last_name are deprecated. Use name instead.", caller)
64
+ if first_name.nil?
65
+ first, last = SolidusPaypalBraintree::Address.split_name(name)
66
+ address.firstname = first
67
+ address.lastname = last || "(left blank)"
68
+ else
69
+ address.firstname = first_name
70
+ address.lastname = last_name || "(left blank)"
71
+ end
72
+ end
51
73
 
52
74
  if spree_state
53
75
  address.state = spree_state
@@ -59,7 +81,7 @@ module SolidusPaypalBraintree
59
81
 
60
82
  # Check to see if this address should match to a state model in the database
61
83
  def should_match_state_model?
62
- spree_country.try!(:states_required?)
84
+ spree_country&.states_required?
63
85
  end
64
86
  end
65
87
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
 
3
5
  module SolidusPaypalBraintree
@@ -10,8 +12,8 @@ module SolidusPaypalBraintree
10
12
  errors.add("Address", "is invalid") if address && !address.valid?
11
13
 
12
14
  if !transaction.valid?
13
- transaction.errors.each do |field, error|
14
- errors.add(field, error)
15
+ transaction.errors.each do |error|
16
+ errors.add(error.attribute, error.message)
15
17
  end
16
18
  end
17
19
  errors.none?
@@ -25,30 +27,33 @@ module SolidusPaypalBraintree
25
27
  end
26
28
 
27
29
  def source
28
- SolidusPaypalBraintree::Source.new nonce: transaction.nonce,
30
+ SolidusPaypalBraintree::Source.new(
31
+ nonce: transaction.nonce,
29
32
  payment_type: transaction.payment_type,
30
33
  payment_method: transaction.payment_method,
34
+ paypal_funding_source: transaction.paypal_funding_source,
31
35
  user: user
36
+ )
32
37
  end
33
38
 
34
- def user
35
- order.user
36
- end
39
+ delegate :user, to: :order
37
40
 
38
41
  def import!(end_state, restart_checkout: false)
39
42
  if valid?
40
- order.email = user.try!(:email) || transaction.email
43
+ order.email = user&.email || transaction.email
41
44
 
42
45
  if address
43
46
  order.shipping_address = order.billing_address = address
44
47
  # work around a bug in most solidus versions
45
48
  # about tax zone cachine between address changes
46
- order.instance_variable_set("@tax_zone", nil)
49
+ order.instance_variable_set(:@tax_zone, nil)
47
50
  end
48
51
 
49
- payment = order.payments.new source: source,
52
+ payment = order.payments.new(
53
+ source: source,
50
54
  payment_method: transaction.payment_method,
51
55
  amount: order.total
56
+ )
52
57
 
53
58
  order.save!
54
59
  order.restart_checkout_flow if restart_checkout
@@ -85,9 +90,9 @@ module SolidusPaypalBraintree
85
90
  payment_total = order.payments.where(state: %w[checkout pending]).sum(:amount)
86
91
  payment_difference = order.outstanding_balance - payment_total
87
92
 
88
- if payment_difference != 0
89
- payment.update!(amount: payment.amount + payment_difference)
90
- end
93
+ return unless payment_difference != 0
94
+
95
+ payment.update!(amount: payment.amount + payment_difference)
91
96
  end
92
97
  end
93
98
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(
4
+ name: "payments/payment/add_paypal_funding_source_to_payment",
5
+ virtual_path: "spree/payments/_payment",
6
+ original: "0b5b5ae53626059cb3a202ef95d10827dd399925",
7
+ insert_after: "erb[loud]:contains('content_tag(:span, payment.payment_method.name)')",
8
+ partial: "solidus_paypal_braintree/payments/payment"
9
+ )
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.call(payment_source, :id, :token, :created_at)
@@ -0,0 +1,10 @@
1
+ <tr id="<%= dom_id(wallet_payment_source, 'spree')%>" class="<%= cycle('even', 'odd') %>">
2
+ <td>
3
+ <%= radio_button_tag "order[wallet_payment_source_id]",
4
+ wallet_payment_source.id,
5
+ default,
6
+ class: "existing-cc-radio" %>
7
+ </td>
8
+ <td><%= wallet_payment_source.payment_source.friendly_payment_type %></td>
9
+ <td><%= wallet_payment_source.payment_source.display_number %></td>
10
+ </tr>
@@ -11,8 +11,8 @@
11
11
  amount: "<%= current_order.total %>",
12
12
  shippingContact: {
13
13
  emailAddress: '<%= current_order.email %>',
14
- familyName: '<%= address.firstname %>',
15
- givenName: '<%= address.lastname %>',
14
+ givenName: '<%= address.firstname %>',
15
+ familyName: '<%= address.lastname %>',
16
16
  phoneNumber: '<%= address.phone %>',
17
17
  addressLines: ['<%= address.address1 %>','<%= address.address2 %>'],
18
18
  locality: '<%= address.city %>',
@@ -1,19 +1,16 @@
1
1
  <script type="text/javascript">
2
- BraintreeError = {
3
- HOSTED_FIELDS_FIELDS_EMPTY: "<%= I18n.t('solidus_paypal_braintree.errors.empty_fields')%>",
4
- HOSTED_FIELDS_FIELDS_INVALID: "<%= I18n.t('solidus_paypal_braintree.errors.invalid_fields')%>",
5
- HOSTED_FIELDS_FAILED_TOKENIZATION: "<%= I18n.t('solidus_paypal_braintree.errors.invalid_card')%>",
6
- HOSTED_FIELDS_TOKENIZATION_NETWORK_ERROR: "<%= I18n.t('solidus_paypal_braintree.errors.network_error')%>",
7
- HOSTED_FIELDS_FIELD_DUPLICATE_IFRAME: "<%= I18n.t('solidus_paypal_braintree.errors.duplicate_iframe')%>",
8
- HOSTED_FIELDS_TOKENIZATION_FAIL_ON_DUPLICATE: "<%= I18n.t('solidus_paypal_braintree.errors.fail_on_duplicate')%>",
9
- HOSTED_FIELDS_TOKENIZATION_CVV_VERIFICATION_FAILED: "<%= I18n.t('solidus_paypal_braintree.errors.cvv_verification_failed')%>",
10
-
11
- getErrorFromSlug: function(slug) {
12
- // Default error message
13
- error = "<%= I18n.t('solidus_paypal_braintree.errors.default_error')%>"
14
- if (slug in BraintreeError)
15
- error = BraintreeError[slug]
16
- return error
17
- }
18
- }
2
+ BraintreeError.DEFAULT = "<%= I18n.t('solidus_paypal_braintree.errors.default_error')%>"
3
+ BraintreeError.PAYPAL_POPUP_CLOSED = "<%= I18n.t('solidus_paypal_braintree.errors.paypal_popup_closed') %>"
4
+ BraintreeError.HOSTED_FIELDS_FIELDS_EMPTY = "<%= I18n.t('solidus_paypal_braintree.errors.empty_fields')%>"
5
+ BraintreeError.HOSTED_FIELDS_FIELDS_INVALID = "<%= I18n.t('solidus_paypal_braintree.errors.invalid_fields')%>"
6
+ BraintreeError.HOSTED_FIELDS_FAILED_TOKENIZATION = "<%= I18n.t('solidus_paypal_braintree.errors.invalid_card')%>"
7
+ BraintreeError.HOSTED_FIELDS_TOKENIZATION_NETWORK_ERROR = "<%= I18n.t('solidus_paypal_braintree.errors.network_error')%>"
8
+ BraintreeError.HOSTED_FIELDS_FIELD_DUPLICATE_IFRAME = "<%= I18n.t('solidus_paypal_braintree.errors.duplicate_iframe')%>"
9
+ BraintreeError.HOSTED_FIELDS_TOKENIZATION_FAIL_ON_DUPLICATE = "<%= I18n.t('solidus_paypal_braintree.errors.fail_on_duplicate')%>"
10
+ BraintreeError.HOSTED_FIELDS_TOKENIZATION_CVV_VERIFICATION_FAILED = "<%= I18n.t('solidus_paypal_braintree.errors.cvv_verification_failed')%>"
11
+ BraintreeError.THREEDS_AUTHENTICATION_FAILED = "<%= t('solidus_paypal_braintree.errors.threeds.authentication_failed') %>";
12
+ BraintreeError.THREEDS_CARDINAL_SDK_ERROR = "<%= t('solidus_paypal_braintree.errors.threeds.authentication_failed') %>";
13
+ BraintreeError.VENMO_CANCELED = "<%= t('solidus_paypal_braintree.errors.venmo.canceled') %>";
14
+ BraintreeError.VENMO_DESKTOP_CANCELED = "<%= t('solidus_paypal_braintree.errors.venmo.desktop_canceled') %>";
15
+ BraintreeError.VENMO_APP_CANCELED = "<%= t('solidus_paypal_braintree.errors.venmo.app_canceled') %>";
19
16
  </script>
@@ -1,24 +1,30 @@
1
- <% prefix = "payment_source[#{id}]" %>
1
+ <% if defined?(id) && !defined?(payment_method) %>
2
+ <% payment_method = Spree::PaymentMethod.find(id) %>
3
+ <% Spree::Deprecation.warn(
4
+ "passing id to _braintree_hosted_fields.html.erb is deprecated. Please pass the payment_method directly.",
5
+ caller
6
+ ) %>
7
+ <% end %>
2
8
 
3
- <%= render partial: "spree/shared/braintree_errors" %>
9
+ <% prefix = "payment_source[#{payment_method.id}]" %>
4
10
 
5
11
  <div class="hosted-fields">
6
12
  <div class="field" data-hook="card_number">
7
- <%= label_tag "card_number#{id}", Spree::CreditCard.human_attribute_name(:number), class: "required" %>
8
- <div class="input" id="card_number<%= id %>"></div>
13
+ <%= label_tag "card_number#{payment_method.id}", Spree::CreditCard.human_attribute_name(:number), class: "required" %>
14
+ <div class="input" id="card_number<%= payment_method.id %>"></div>
9
15
  </div>
10
16
 
11
17
  <div class="field" data-hook="card_expiration">
12
- <%= label_tag "card_expiry#{id}", Spree::CreditCard.human_attribute_name(:expiration), class: "required" %>
13
- <div class="input" id="card_expiry<%= id %>"></div>
18
+ <%= label_tag "card_expiry#{payment_method.id}", Spree::CreditCard.human_attribute_name(:expiration), class: "required" %>
19
+ <div class="input" id="card_expiry<%= payment_method.id %>"></div>
14
20
  </div>
15
21
 
16
22
  <div class="field" data-hook="card_code">
17
- <%= label_tag "card_code#{id}", Spree::CreditCard.human_attribute_name(:card_code), class: "required" %>
18
- <div class="input" id="card_code<%= id %>"></div>
23
+ <%= label_tag "card_code#{payment_method.id}", Spree::CreditCard.human_attribute_name(:card_code), class: "required" %>
24
+ <div class="input" id="card_code<%= payment_method.id %>"></div>
19
25
 
20
26
  <a href="/content/cvv" class="info cvvLink" target="_blank">
21
- (<%= Spree.t(:what_is_this) %>)
27
+ (<%= I18n.t("spree.what_is_this") %>)
22
28
  </a>
23
29
  </div>
24
30
 
@@ -26,3 +32,12 @@
26
32
  <input type="hidden" name="<%= prefix %>[payment_type]" value="<%= SolidusPaypalBraintree::Source::CREDIT_CARD %>">
27
33
  <input type="hidden" id="payment_method_nonce" name="<%= prefix %>[nonce]">
28
34
  </div>
35
+
36
+
37
+ <script>
38
+ <% if current_store.braintree_configuration.three_d_secure? %>
39
+ var threeDSecureOptions = <%= raw braintree_3ds_options_for(current_order).to_json %>;
40
+ <% end -%>
41
+ var credit_card_fields_style = <%= raw payment_method.preferred_credit_card_fields_style.to_json %>
42
+ var placeholder_text = <%= raw payment_method.preferred_placeholder_text.to_json %>
43
+ </script>