diff --git a/README.md b/README.md index 4fe9c080..a3c22125 100644 --- a/README.md +++ b/README.md @@ -123,14 +123,12 @@ PayPals API does not allow for admin-side payments. Instead, backend users takin ## Venmo Venmo is currently available to US merchants and buyers. There are also other [prequisites](https://developer.paypal.com/docs/business/checkout/pay-with-venmo/#eligibility). -If the transaction supports Venmo then a button should appear for it on checkout, cart and product page, depending on your `Payment Method` preferences. +If the transaction supports Venmo and it is enabled by the following, then a button should appear for it on checkout payment page. Note, Venmo cannot currently be rendered on the product or cart pages. -By default, the extension and PayPal will try to render a Venmo button to buyers when prequisites are met. - -Set the PaypalCommercePlatform `PaymentMethod` `venmo_control` preference to: -- `enforced`, disregard buyer's location and show button (if other prequisites are met); or -- `enabled` (default), available as a PayPal funding option (if other prequisites are met); or -- `disabled`, disable Venmo as a funding option. +Set the PaypalCommercePlatform `PaymentMethod` `venmo_standalone` preference to: +- `render only standalone`, show Venmo on the payment page and do not show the other funding sources (i.e. PayPal, Credit); or +- `enabled`, show Venmo on the payment page; or +- `disabled` (default), do not show the Venmo button. See more about preferences([Configuration](#configuration)) below. diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/button_actions.js b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/button_actions.js index c62acf91..dbfbecca 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/button_actions.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/button_actions.js @@ -84,6 +84,7 @@ SolidusPaypalCommercePlatform.approveOrder = function(data, actions) { SolidusPaypalCommercePlatform.verifyTotal(response.purchase_units[0].amount.value).then(function(){ $("#payments_source_paypal_order_id").val(data.orderID) $("#payments_source_paypal_email").val(response.payer.email_address) + $("#payments_source_paypal_funding_source").val(SolidusPaypalCommercePlatform.fundingSource) $("#checkout_form_payment").submit() }) }) @@ -168,7 +169,8 @@ SolidusPaypalCommercePlatform.addPayment = function(paypal_amount, payment_metho payment_method_id: payment_method_id, source_attributes: { paypal_order_id: data.orderID, - paypal_email: email + paypal_email: email, + paypal_funding_source: SolidusPaypalCommercePlatform.fundingSource } } }, diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/buttons.js b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/buttons.js index 5d020bd6..f08a4c5d 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/buttons.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/buttons.js @@ -2,6 +2,7 @@ SolidusPaypalCommercePlatform.renderButton = function(payment_method_id, style) paypal.Buttons({ style: style, createOrder: SolidusPaypalCommercePlatform.sendOrder.bind(null, payment_method_id), + onClick: (data) => { SolidusPaypalCommercePlatform.fundingSource = data.fundingSource }, onApprove: SolidusPaypalCommercePlatform.approveOrder, onShippingChange: SolidusPaypalCommercePlatform.shippingChange, onError: SolidusPaypalCommercePlatform.handleError @@ -12,6 +13,7 @@ SolidusPaypalCommercePlatform.renderCartButton = function(payment_method_id, sty paypal.Buttons({ style: style, createOrder: SolidusPaypalCommercePlatform.sendOrder.bind(null, payment_method_id), + onClick: (data) => { SolidusPaypalCommercePlatform.fundingSource = data.fundingSource }, onApprove: SolidusPaypalCommercePlatform.finalizeOrder.bind(null, payment_method_id), onShippingChange: SolidusPaypalCommercePlatform.shippingChange, onError: SolidusPaypalCommercePlatform.handleError @@ -22,8 +24,20 @@ SolidusPaypalCommercePlatform.renderProductButton = function(payment_method_id, paypal.Buttons({ style: style, createOrder: SolidusPaypalCommercePlatform.createAndSendOrder.bind(null, payment_method_id), + onClick: (data) => { SolidusPaypalCommercePlatform.fundingSource = data.fundingSource }, onApprove: SolidusPaypalCommercePlatform.finalizeOrder.bind(null, payment_method_id), onShippingChange: SolidusPaypalCommercePlatform.shippingChange, onError: SolidusPaypalCommercePlatform.handleError }).render('#paypal-button-container') } + +SolidusPaypalCommercePlatform.renderVenmoStandalone = function(payment_method_id, style) { + paypal.Buttons({ + style: style, + fundingSource: paypal.FUNDING.VENMO, + createOrder: SolidusPaypalCommercePlatform.sendOrder.bind(null, payment_method_id), + onClick: () => { SolidusPaypalCommercePlatform.fundingSource = paypal.FUNDING.VENMO }, + onApprove: SolidusPaypalCommercePlatform.approveOrder, + onError: SolidusPaypalCommercePlatform.handleError + }).render('#paypal-button-container') +} diff --git a/app/helpers/solidus_paypal_commerce_platform/button_options_helper.rb b/app/helpers/solidus_paypal_commerce_platform/button_options_helper.rb index a437aefc..647c24b8 100644 --- a/app/helpers/solidus_paypal_commerce_platform/button_options_helper.rb +++ b/app/helpers/solidus_paypal_commerce_platform/button_options_helper.rb @@ -17,5 +17,9 @@ def preferred_paypal_button_shape_options def preferred_paypal_button_layout_options [["Vertical", "vertical"], ["Horizontal", "horizontal"]] end + + def preferred_venmo_standalone_options + [['Render only standalone', 'render only standalone'], ['Enabled', 'Enabled'], ['Disabled', 'disabled']] + end end end diff --git a/app/models/solidus_paypal_commerce_platform/payment_method.rb b/app/models/solidus_paypal_commerce_platform/payment_method.rb index f8680ce8..3555887a 100644 --- a/app/models/solidus_paypal_commerce_platform/payment_method.rb +++ b/app/models/solidus_paypal_commerce_platform/payment_method.rb @@ -12,13 +12,21 @@ class PaymentMethod < SolidusSupport.payment_method_parent_class preference :display_on_cart, :boolean, default: true preference :display_on_product_page, :boolean, default: true preference :display_credit_messaging, :boolean, default: true - preference :venmo_control, :string, default: 'enabled' + preference :venmo_standalone, :paypal_select, default: 'disabled' preference :force_buyer_country, :string - validates :preferred_venmo_control, inclusion: { - in: %w[enforced enabled disabled], - message: "must be 'enforced', 'enabled' or 'disabled'." - } + validates :preferred_paypal_button_color, exclusion: { + in: %w[gold], + message: "cannot be 'gold' when Venmo standalone is enabled." + }, if: :venmo_standalone_enabled? + + def venmo_standalone_enabled? + options[:venmo_standalone] != 'disabled' + end + + def render_only_venmo_standalone? + options[:venmo_standalone] == 'render only standalone' + end def partial_name "paypal_commerce_platform" @@ -79,8 +87,7 @@ def javascript_sdk_url(order: nil, currency: nil) currency: currency } - parameters['enable-funding'] = 'venmo' if options[:venmo_control] == 'enforced' - parameters['disable-funding'] = 'venmo' if options[:venmo_control] == 'disabled' + parameters['enable-funding'] = 'venmo' if venmo_standalone_enabled? if !Rails.env.production? && options[:force_buyer_country].present? parameters['buyer-country'] = options[:force_buyer_country] diff --git a/app/models/solidus_paypal_commerce_platform/payment_source.rb b/app/models/solidus_paypal_commerce_platform/payment_source.rb index 569b7607..b5bb1a7e 100644 --- a/app/models/solidus_paypal_commerce_platform/payment_source.rb +++ b/app/models/solidus_paypal_commerce_platform/payment_source.rb @@ -3,8 +3,19 @@ module SolidusPaypalCommercePlatform class PaymentSource < SolidusSupport.payment_source_parent_class self.table_name = "paypal_commerce_platform_sources" + enum paypal_funding_source: { + applepay: 0, bancontact: 1, blik: 2, boleto: 3, card: 4, credit: 5, eps: 6, giropay: 7, ideal: 8, + itau: 9, maxima: 10, mercadopago: 11, mybank: 12, oxxo: 13, p24: 14, paylater: 15, paypal: 16, payu: 17, + sepa: 18, sofort: 19, trustly: 20, venmo: 21, verkkopankki: 22, wechatpay: 23, zimpler: 24 + }, _suffix: :funding validates :paypal_order_id, :payment_method_id, presence: true + def display_paypal_funding_source + I18n.t(paypal_funding_source, + scope: 'solidus_paypal_commerce_platform.paypal_funding_sources', + default: paypal_funding_source) + end + def actions %w(capture void credit) end diff --git a/config/locales/en.yml b/config/locales/en.yml index 235b94c8..f896f106 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -18,6 +18,8 @@ en: paypal_capture_id: PayPal Capture ID paypal_debug_id: PayPal Debug ID paypal_email: PayPal Email + paypal_funding: 'PayPal Funding: %{funding}' + paypal_funding_source: 'PayPal Funding Source' payment_email: "Payment Email: %{email}" virtual_terminal_notice_html: Backend Credit Card payments can only be accepted through PayPal via the PayPal Virtual Terminal. solidus_paypal_commerce_platform: @@ -34,4 +36,29 @@ en: success: "Payment refunded for %{amount}" pay_pal_checkout_sdk/payments/authorizations_void_request: success: "Payment voided" - + paypal_funding_sources: + applepay: 'Apple Pay' + bancontact: 'Bancontact' + blik: 'BLIK' + boleto: 'Boleto' + card: 'Credit or debit card' + credit: 'PayPal Credit' + eps: 'eps' + giropay: 'giropay' + ideal: 'iDEAL' + itau: 'Itau' + maxima: 'Maxima' + mercadopago: 'Mercado Pago' + mybank: 'MyBank' + oxxo: 'Oxxo' + p24: 'Przelewy24' + paylater: 'Pay Later' + paypal: 'PayPal' + payu: 'PayU' + sepa: 'SEPA-Lastschrift' + sofort: 'Sofort' + trustly: 'Trustly' + venmo: 'Venmo' + verkkopankki: 'Verkkopankki' + wechatpay: 'WeChat Pay' + zimpler: 'Zimpler' diff --git a/db/migrate/20211220133406_add_paypal_funding_source_to_paypal_commerce_platform_sources.rb b/db/migrate/20211220133406_add_paypal_funding_source_to_paypal_commerce_platform_sources.rb new file mode 100644 index 00000000..7ff5741a --- /dev/null +++ b/db/migrate/20211220133406_add_paypal_funding_source_to_paypal_commerce_platform_sources.rb @@ -0,0 +1,5 @@ +class AddPaypalFundingSourceToPaypalCommercePlatformSources < ActiveRecord::Migration[6.1] + def change + add_column :paypal_commerce_platform_sources, :paypal_funding_source, :integer + end +end diff --git a/lib/solidus_paypal_commerce_platform/engine.rb b/lib/solidus_paypal_commerce_platform/engine.rb index 67b3a260..c338ab25 100644 --- a/lib/solidus_paypal_commerce_platform/engine.rb +++ b/lib/solidus_paypal_commerce_platform/engine.rb @@ -17,7 +17,8 @@ class Engine < Rails::Engine initializer "solidus_paypal_commerce_platform.add_payment_method", after: "spree.register.payment_methods" do |app| app.config.spree.payment_methods << SolidusPaypalCommercePlatform::PaymentMethod SolidusPaypalCommercePlatform::PaymentMethod.allowed_admin_form_preference_types << :paypal_select - Spree::PermittedAttributes.source_attributes.concat [:paypal_order_id, :authorization_id, :paypal_email] + Spree::PermittedAttributes.source_attributes.concat [:paypal_order_id, :authorization_id, + :paypal_email, :paypal_funding_source] end initializer "solidus_paypal_commerce_platform.add_wizard", after: "spree.register.payment_methods" do |app| diff --git a/lib/solidus_paypal_commerce_platform/testing_support/factories.rb b/lib/solidus_paypal_commerce_platform/testing_support/factories.rb index 6918a610..708573a5 100644 --- a/lib/solidus_paypal_commerce_platform/testing_support/factories.rb +++ b/lib/solidus_paypal_commerce_platform/testing_support/factories.rb @@ -7,7 +7,8 @@ preferences { { client_id: SecureRandom.hex(8), - client_secret: SecureRandom.hex(10) + client_secret: SecureRandom.hex(10), + venmo_standalone: 'disabled' } } end diff --git a/lib/views/backend/spree/admin/payments/source_views/_paypal_commerce_platform.html.erb b/lib/views/backend/spree/admin/payments/source_views/_paypal_commerce_platform.html.erb index cdd70ca8..bd442486 100644 --- a/lib/views/backend/spree/admin/payments/source_views/_paypal_commerce_platform.html.erb +++ b/lib/views/backend/spree/admin/payments/source_views/_paypal_commerce_platform.html.erb @@ -12,6 +12,9 @@
<%= I18n.t("paypal_email") %>:
<%= payment.source.paypal_email %>
+ +
<%= I18n.t("paypal_funding_source") %>:
+
<%= payment.source.display_paypal_funding_source %>
diff --git a/lib/views/frontend/solidus_paypal_commerce_platform/payments/_payment.html.erb b/lib/views/frontend/solidus_paypal_commerce_platform/payments/_payment.html.erb index a0095af2..286717af 100644 --- a/lib/views/frontend/solidus_paypal_commerce_platform/payments/_payment.html.erb +++ b/lib/views/frontend/solidus_paypal_commerce_platform/payments/_payment.html.erb @@ -1,3 +1,7 @@ +<% if payment.source.respond_to?(:paypal_funding_source) %> +
+ <%= t('paypal_funding', funding: payment.source.display_paypal_funding_source) %> +<% end %> <% if payment.source.respond_to?(:paypal_email) %>
<%= t('payment_email', email: payment.source.paypal_email) %> diff --git a/lib/views/frontend/spree/checkout/payment/_paypal_commerce_platform.html.erb b/lib/views/frontend/spree/checkout/payment/_paypal_commerce_platform.html.erb index b9fdbb38..6ffba4b0 100644 --- a/lib/views/frontend/spree/checkout/payment/_paypal_commerce_platform.html.erb +++ b/lib/views/frontend/spree/checkout/payment/_paypal_commerce_platform.html.erb @@ -6,13 +6,27 @@ + +<% unless payment_method.render_only_venmo_standalone? %> + +<% end %> + +<% if payment_method.venmo_standalone_enabled? %> + +<% end %> +
diff --git a/spec/features/frontend/checkout_spec.rb b/spec/features/frontend/checkout_spec.rb index 20b551ab..57ee94f4 100644 --- a/spec/features/frontend/checkout_spec.rb +++ b/spec/features/frontend/checkout_spec.rb @@ -70,6 +70,16 @@ click_button("Save and Continue") expect(Spree::Payment.last.source.paypal_email).to eq "fake@email.com" end + + it "records the paypal funding source" do + visit '/checkout/payment' + choose(option: paypal_payment_method.id) + find(:xpath, "//input[@id='payments_source_paypal_order_id']", visible: false).set SecureRandom.hex(8) + find(:xpath, "//input[@id='payments_source_paypal_email']", visible: false).set "fake@email.com" + find(:xpath, "//input[@id='payments_source_paypal_funding_source']", visible: false).set "venmo" + click_button("Save and Continue") + expect(Spree::Payment.last.source.paypal_funding_source).to eq "venmo" + end end context "when a payment fails" do diff --git a/spec/models/solidus_paypal_commerce_platform/payment_method_spec.rb b/spec/models/solidus_paypal_commerce_platform/payment_method_spec.rb index 5c42c0a1..9e5ebd6b 100644 --- a/spec/models/solidus_paypal_commerce_platform/payment_method_spec.rb +++ b/spec/models/solidus_paypal_commerce_platform/payment_method_spec.rb @@ -15,6 +15,23 @@ def Struct(data) # rubocop:disable Naming/MethodName before { allow_any_instance_of(PayPal::PayPalHttpClient).to receive(:execute) { response } } + describe 'preferences' do + context 'with paypal_button_color' do + before do + paypal_payment_method.preferences.update(paypal_button_color: 'gold') + paypal_payment_method.save + end + + it 'cannot be gold when Venmo standalone is enabled' do + expect(paypal_payment_method).to be_valid + paypal_payment_method.preferences.update(venmo_standalone: 'enabled') + expect(paypal_payment_method).to be_invalid + paypal_payment_method.preferences.update(venmo_standalone: 'only render standalone') + expect(paypal_payment_method).to be_invalid + end + end + end + describe "#purchase" do let(:result) { Struct(purchase_units: [Struct(payments: payments)]) } let(:payments) { Struct(captures: [Struct(id: SecureRandom.hex(4))]) } @@ -107,64 +124,28 @@ def Struct(data) # rubocop:disable Naming/MethodName end end - describe 'preferences' do - context 'with venmo_control' do - it 'is "default" by default' do - expect(paypal_payment_method.preferred_venmo_control).to eq('enabled') - end - - it 'only allows "enforced", "enabled" and "disabled" as values', :aggregate_failures do - expect(paypal_payment_method).to be_valid - valid_values = %w[enforced enabled disabled] - invalid_values = %w[foo bar] - - valid_values.each do |value| - paypal_payment_method.preferred_venmo_control = value - expect(paypal_payment_method).to be_valid - end - - invalid_values.each do |value| - paypal_payment_method.preferred_venmo_control = value - expect(paypal_payment_method).to be_invalid - end - end - end - end - - context 'when venmo_control is "enforced"' do - before { paypal_payment_method.preferences.update(venmo_control: 'enforced') } + context 'when venmo_standalone is "only render standalone"' do + before { paypal_payment_method.preferences.update(venmo_standalone: 'only render standalone') } it 'includes "enable-funding=venmo" as a parameter' do expect(url.query.split('&')).to include('enable-funding=venmo') end - - it 'does not include the "disable-funding" parameter' do - expect(url.query.split('&')).not_to include(match 'disable-funding') - end end - context 'when venmo_control is "enabled"' do - before { paypal_payment_method.preferences.update(venmo_control: 'enabled') } - - it 'include the "enable-funding" parameter' do - expect(url.query.split('&')).not_to include(match 'enable-funding') - end + context 'when venmo_standalone is "enabled"' do + before { paypal_payment_method.preferences.update(venmo_standalone: 'enabled') } - it 'does not include the "disable-funding" parameter' do - expect(url.query.split('&')).not_to include(match 'disable-funding') + it 'includes "enable-funding=venmo" as a parameter' do + expect(url.query.split('&')).to include('enable-funding=venmo') end end - context 'when venmo_control is "disabled"' do - before { paypal_payment_method.preferences.update(venmo_control: 'disabled') } + context 'when venmo_standalone is "disabled"' do + before { paypal_payment_method.preferences.update(venmo_standalone: 'disabled') } it 'does not include the "enable-funding" parameter' do expect(url.query.split('&')).not_to include(match 'enable-funding') end - - it 'includes "disable-funding=venmo" as a parameter' do - expect(url.query.split('&')).to include('disable-funding=venmo') - end end context 'when force_buyer_country is an empty string' do diff --git a/spec/models/solidus_paypal_commerce_platform/payment_source_spec.rb b/spec/models/solidus_paypal_commerce_platform/payment_source_spec.rb index d5506216..e479b927 100644 --- a/spec/models/solidus_paypal_commerce_platform/payment_source_spec.rb +++ b/spec/models/solidus_paypal_commerce_platform/payment_source_spec.rb @@ -61,5 +61,56 @@ expect(payment.actions).not_to include("void") end end + + context 'with #display_paypal_funding_source' do + context 'when the EN locale exists' do + it 'translates the funding source' do + payment_source.paypal_funding_source = 'card' + + result = payment_source.display_paypal_funding_source + + expect(result).to eq('Credit or debit card') + end + end + + context "when the locale doesn't exist" do + it 'returns the paypal_funding_source as the default' do + allow(payment_source).to receive(:paypal_funding_source).and_return('non-existent') + + result = payment_source.display_paypal_funding_source + + expect(result).to eq('non-existent') + end + end + end + end + + describe 'attributes' do + context 'with paypal_funding_source' do + it 'can be nil' do + payment_source.paypal_funding_source = nil + + expect(payment_source).to be_valid + end + + it 'makes empty strings nil' do + payment_source.paypal_funding_source = '' + + result = payment_source.save + + expect(result).to eq(true) + expect(payment_source.paypal_funding_source).to be_nil + end + + it 'gets correctly mapped as an enum' do + payment_source.paypal_funding_source = 'applepay' + + result = payment_source.save + + expect(result).to eq(true) + expect(payment_source.paypal_funding_source).to eq('applepay') + expect(payment_source.applepay_funding?).to eq(true) + end + end end end