solidus_stripe 3.0.0 → 4.1.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +5 -0
  4. data/CHANGELOG.md +55 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE +2 -2
  7. data/README.md +69 -11
  8. data/Rakefile +1 -1
  9. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +42 -9
  10. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +61 -25
  11. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +4 -2
  12. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +56 -19
  13. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +7 -1
  14. data/app/controllers/solidus_stripe/intents_controller.rb +42 -28
  15. data/app/decorators/models/spree/order_update_attributes_decorator.rb +39 -0
  16. data/app/decorators/models/spree/payment_decorator.rb +11 -0
  17. data/app/decorators/models/spree/refund_decorator.rb +9 -0
  18. data/app/models/solidus_stripe/address_from_params_service.rb +5 -2
  19. data/app/models/solidus_stripe/create_intents_payment_service.rb +113 -0
  20. data/app/models/spree/payment_method/stripe_credit_card.rb +19 -9
  21. data/bin/r +13 -0
  22. data/bin/rake +7 -0
  23. data/bin/sandbox +84 -0
  24. data/bin/sandbox_rails +18 -0
  25. data/bin/setup +1 -1
  26. data/config/routes.rb +4 -1
  27. data/lib/generators/solidus_stripe/install/install_generator.rb +7 -3
  28. data/lib/solidus_stripe/engine.rb +2 -2
  29. data/lib/solidus_stripe/factories.rb +4 -0
  30. data/lib/solidus_stripe/version.rb +1 -1
  31. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -1
  32. data/solidus_stripe.gemspec +34 -37
  33. data/spec/features/stripe_checkout_spec.rb +159 -64
  34. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +19 -5
  35. data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +127 -0
  36. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +44 -1
  37. data/spec/spec_helper.rb +4 -1
  38. metadata +22 -12
  39. data/LICENSE.md +0 -26
data/bin/r ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/solidus_stripe/engine', __dir__)
7
+
8
+ # Set up gems listed in the Gemfile.
9
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
10
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11
+
12
+ require 'rails/all'
13
+ require 'rails/engine/commands'
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ load Gem.bin_path("rake", "rake")
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ case "$DB" in
6
+ postgres|postgresql)
7
+ RAILSDB="postgresql"
8
+ ;;
9
+ mysql)
10
+ RAILSDB="mysql"
11
+ ;;
12
+ sqlite|'')
13
+ RAILSDB="sqlite3"
14
+ ;;
15
+ *)
16
+ echo "Invalid DB specified: $DB"
17
+ exit 1
18
+ ;;
19
+ esac
20
+
21
+ if [ ! -z $SOLIDUS_BRANCH ]
22
+ then
23
+ BRANCH=$SOLIDUS_BRANCH
24
+ else
25
+ BRANCH="master"
26
+ fi
27
+
28
+ extension_name="solidus_stripe"
29
+
30
+ # Stay away from the bundler env of the containing extension.
31
+ function unbundled {
32
+ ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@
33
+ }
34
+
35
+ rm -rf ./sandbox
36
+ unbundled bundle exec rails new sandbox --database="$RAILSDB" \
37
+ --skip-bundle \
38
+ --skip-git \
39
+ --skip-keeps \
40
+ --skip-rc \
41
+ --skip-spring \
42
+ --skip-test \
43
+ --skip-javascript
44
+
45
+ if [ ! -d "sandbox" ]; then
46
+ echo 'sandbox rails application failed'
47
+ exit 1
48
+ fi
49
+
50
+ cd ./sandbox
51
+ cat <<RUBY >> Gemfile
52
+ gem 'solidus', github: 'solidusio/solidus', branch: '$BRANCH'
53
+ gem 'solidus_auth_devise', '>= 2.1.0'
54
+ gem 'rails-i18n'
55
+ gem 'solidus_i18n'
56
+
57
+ gem '$extension_name', path: '..'
58
+
59
+ group :test, :development do
60
+ platforms :mri do
61
+ gem 'pry-byebug'
62
+ end
63
+ end
64
+ RUBY
65
+
66
+ unbundled bundle install --gemfile Gemfile
67
+
68
+ unbundled bundle exec rake db:drop db:create
69
+
70
+ unbundled bundle exec rails generate spree:install \
71
+ --auto-accept \
72
+ --user_class=Spree::User \
73
+ --enforce_available_locales=true \
74
+ --with-authentication=false \
75
+ $@
76
+
77
+ unbundled bundle exec rails generate solidus:auth:install
78
+
79
+ echo
80
+ echo "🚀 Sandbox app successfully created for $extension_name!"
81
+ echo "🚀 Using $RAILSDB and Solidus $BRANCH"
82
+ echo "🚀 Use 'export DB=[postgres|mysql|sqlite]' to control the DB adapter"
83
+ echo "🚀 Use 'export SOLIDUS_BRANCH=<BRANCH-NAME>' to control the Solidus version"
84
+ echo "🚀 This app is intended for test purposes."
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ app_root = 'sandbox'
6
+
7
+ unless File.exist? "#{app_root}/bin/rails"
8
+ warn 'Creating the sandbox app...'
9
+ Dir.chdir "#{__dir__}/.." do
10
+ system "#{__dir__}/sandbox" or begin # rubocop:disable Style/AndOr
11
+ warn 'Automatic creation of the sandbox app failed'
12
+ exit 1
13
+ end
14
+ end
15
+ end
16
+
17
+ Dir.chdir app_root
18
+ exec 'bin/rails', *ARGV
data/bin/setup CHANGED
@@ -5,4 +5,4 @@ set -vx
5
5
 
6
6
  gem install bundler --conservative
7
7
  bundle update
8
- bundle exec rake extension:test_app
8
+ bin/rake clobber
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Spree::Core::Engine.routes.draw do
2
4
  # route to a deprecated controller, will be removed in the future:
3
5
  post '/stripe/confirm_payment', to: 'stripe#confirm_payment'
4
6
 
5
7
  # payment intents routes:
6
- post '/stripe/confirm_intents', to: '/solidus_stripe/intents#confirm'
8
+ post '/stripe/create_intent', to: '/solidus_stripe/intents#create_intent'
9
+ post '/stripe/create_payment', to: '/solidus_stripe/intents#create_payment'
7
10
 
8
11
  # payment request routes:
9
12
  post '/stripe/shipping_rates', to: '/solidus_stripe/payment_request#shipping_rates'
@@ -13,12 +13,16 @@ module SolidusStripe
13
13
  run 'bundle exec rake railties:install:migrations FROM=solidus_stripe'
14
14
  end
15
15
 
16
+ def add_migrations
17
+ run 'bin/rails railties:install:migrations FROM=solidus_stripe'
18
+ end
19
+
16
20
  def run_migrations
17
- run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
21
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) # rubocop:disable Metrics/LineLength
18
22
  if run_migrations
19
- run 'bundle exec rake db:migrate'
23
+ run 'bin/rails db:migrate'
20
24
  else
21
- puts 'Skipping rake db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
25
+ puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
22
26
  end
23
27
  end
24
28
 
@@ -4,9 +4,9 @@ require 'spree/core'
4
4
 
5
5
  module SolidusStripe
6
6
  class Engine < Rails::Engine
7
- include SolidusSupport::EngineExtensions::Decorators
7
+ include SolidusSupport::EngineExtensions
8
8
 
9
- isolate_namespace Spree
9
+ isolate_namespace ::Spree
10
10
 
11
11
  engine_name 'solidus_stripe'
12
12
 
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidusStripe
4
- VERSION = "3.0.0"
4
+ VERSION = "4.1.0"
5
5
  end
@@ -1,6 +1,5 @@
1
1
  <div id="payment-request-button" data-stripe-config="<%= payment_method.stripe_config(current_order).to_json %>" data-v3-api="<%= stripe_v3_api %>"></div>
2
2
 
3
- <%= image_tag 'credit_cards/credit_card.gif', id: 'credit-card-image' %>
4
3
  <% param_prefix = "payment_source[#{payment_method.id}]" %>
5
4
 
6
5
  <div class="field field-required">
@@ -1,40 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- $:.push File.expand_path('lib', __dir__)
4
- require 'solidus_stripe/version'
5
-
6
- Gem::Specification.new do |s|
7
- s.name = 'solidus_stripe'
8
- s.version = SolidusStripe::VERSION
9
- s.summary = "Stripe Payment Method for Solidus"
10
- s.description = s.summary
11
- s.required_ruby_version = ">= 2.2"
12
-
13
- s.author = "Solidus Team"
14
- s.email = "contact@solidus.io"
15
- s.homepage = "https://solidus.io"
16
- s.license = 'BSD-3'
17
-
18
- if s.respond_to?(:metadata)
19
- s.metadata["homepage_uri"] = s.homepage if s.homepage
20
- s.metadata["source_code_uri"] = s.homepage if s.homepage
21
- end
22
-
23
- s.files = `git ls-files`.split("\n")
24
- s.test_files = `git ls-files -- spec/*`.split("\n")
25
- s.require_path = "lib"
26
- s.requirements << "none"
27
-
28
- s.bindir = "exe"
29
- s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
-
31
- s.add_dependency 'solidus_core', ['>= 2.3', '< 3']
32
- s.add_dependency 'solidus_support', '~> 0.4.0'
33
- # ActiveMerchant v1.58 through v1.59 introduced a breaking change
34
- # to the stripe gateway.
35
- #
36
- # This was resolved in v1.60, but we still need to skip 1.58 & 1.59.
37
- s.add_dependency "activemerchant", ">= 1.100" # includes "Stripe Payment Intents: Fix fallback for Store"
38
-
39
- s.add_development_dependency 'solidus_dev_support'
3
+ require_relative 'lib/solidus_stripe/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'solidus_stripe'
7
+ spec.version = SolidusStripe::VERSION
8
+ spec.authors = ['Solidus Team']
9
+ spec.email = 'contact@solidus.io'
10
+
11
+ spec.summary = 'Stripe Payment Method for Solidus'
12
+ spec.description = 'Stripe Payment Method for Solidus'
13
+ spec.homepage = 'https://github.com/solidusio/solidus_stripe#readme'
14
+ spec.license = 'BSD-3'
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = 'https://github.com/solidusio/solidus_stripe'
18
+ spec.metadata['changelog_uri'] = 'https://github.com/solidusio/solidus_stripe/blob/master/CHANGELOG.md'
19
+
20
+ spec.required_ruby_version = Gem::Requirement.new('~> 2.4')
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
25
+
26
+ spec.files = files.grep_v(%r{^(test|spec|features)/})
27
+ spec.test_files = files.grep(%r{^(test|spec|features)/})
28
+ spec.bindir = "exe"
29
+ spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency 'solidus_core', ['>= 2.3', '< 3']
33
+ spec.add_dependency 'solidus_support', '~> 0.5'
34
+ spec.add_dependency 'activemerchant', '>= 1.100'
35
+
36
+ spec.add_development_dependency 'solidus_dev_support'
40
37
  end
@@ -6,6 +6,8 @@ RSpec.describe "Stripe checkout", type: :feature do
6
6
  let(:zone) { FactoryBot.create(:zone) }
7
7
  let(:country) { FactoryBot.create(:country) }
8
8
 
9
+ let(:card_3d_secure) { "4000 0025 0000 3155" }
10
+
9
11
  before do
10
12
  FactoryBot.create(:store)
11
13
  zone.members << Spree::ZoneMember.create!(zoneable: country)
@@ -215,7 +217,7 @@ RSpec.describe "Stripe checkout", type: :feature do
215
217
  end
216
218
  end
217
219
 
218
- context 'when using Stripe V3 API libarary with Elements', :js do
220
+ context 'when using Stripe V3 API library with Elements', :js do
219
221
  let(:preferred_v3_elements) { true }
220
222
  let(:preferred_v3_intents) { false }
221
223
 
@@ -291,7 +293,7 @@ RSpec.describe "Stripe checkout", type: :feature do
291
293
  it_behaves_like "Stripe Elements invalid payments"
292
294
  end
293
295
 
294
- context "when using Stripe V3 API libarary with Intents", :js do
296
+ context "when using Stripe V3 API library with Intents", :js do
295
297
  let(:preferred_v3_elements) { false }
296
298
  let(:preferred_v3_intents) { true }
297
299
 
@@ -301,24 +303,8 @@ RSpec.describe "Stripe checkout", type: :feature do
301
303
  end
302
304
 
303
305
  context "when using a valid 3D Secure card" do
304
- let(:card_number) { "4000 0027 6000 3184" }
305
-
306
306
  it "successfully completes the checkout" do
307
- within_frame find('#card_number iframe') do
308
- card_number.split('').each { |n| find_field('cardnumber').native.send_keys(n) }
309
- end
310
- within_frame(find '#card_cvc iframe') { fill_in 'cvc', with: '123' }
311
- within_frame(find '#card_expiry iframe') do
312
- '0132'.split('').each { |n| find_field('exp-date').native.send_keys(n) }
313
- end
314
-
315
- click_button "Save and Continue"
316
-
317
- within_3d_secure_modal do
318
- expect(page).to have_content '$19.99 using 3D Secure'
319
-
320
- click_button 'Complete authentication'
321
- end
307
+ authenticate_3d_secure_card(card_3d_secure)
322
308
 
323
309
  expect(page).to have_current_path("/checkout/confirm")
324
310
 
@@ -368,69 +354,178 @@ RSpec.describe "Stripe checkout", type: :feature do
368
354
  end
369
355
  end
370
356
 
371
- it "can re-use saved cards" do
372
- within_frame find('#card_number iframe') do
373
- "4000 0027 6000 3184".split('').each { |n| find_field('cardnumber').native.send_keys(n) }
374
- end
375
- within_frame(find '#card_cvc iframe') { fill_in 'cvc', with: '123' }
376
- within_frame(find '#card_expiry iframe') do
377
- '0132'.split('').each { |n| find_field('exp-date').native.send_keys(n) }
378
- end
379
- click_button "Save and Continue"
357
+ context "when reusing a card" do
358
+ stub_authorization!
359
+
360
+ it "succesfully creates a second payment that can be captured in the backend" do
361
+ authenticate_3d_secure_card(card_3d_secure)
362
+
363
+ expect(page).to have_current_path("/checkout/confirm")
364
+ click_button "Place Order"
365
+ expect(page).to have_content("Your order has been processed successfully")
366
+
367
+ visit spree.root_path
368
+ click_link "DL-44"
369
+ click_button "Add To Cart"
370
+
371
+ expect(page).to have_current_path("/cart")
372
+ click_button "Checkout"
373
+
374
+ # Address
375
+ expect(page).to have_current_path("/checkout/address")
376
+
377
+ within("#billing") do
378
+ fill_in_name
379
+ fill_in "Street Address", with: "YT-1300"
380
+ fill_in "City", with: "Mos Eisley"
381
+ select "United States of America", from: "Country"
382
+ select country.states.first.name, from: "order_bill_address_attributes_state_id"
383
+ fill_in "Zip", with: "12010"
384
+ fill_in "Phone", with: "(555) 555-5555"
385
+ end
386
+ click_on "Save and Continue"
387
+
388
+ # Delivery
389
+ expect(page).to have_current_path("/checkout/delivery")
390
+ expect(page).to have_content("UPS Ground")
391
+ click_on "Save and Continue"
392
+
393
+ # Payment
394
+ expect(page).to have_current_path("/checkout/payment")
395
+ choose "Use an existing card on file"
396
+ click_button "Save and Continue"
397
+
398
+ # Confirm
399
+ expect(page).to have_current_path("/checkout/confirm")
400
+ click_button "Place Order"
401
+ expect(page).to have_content("Your order has been processed successfully")
402
+
403
+ Spree::Order.complete.each do |order|
404
+ # Capture in backend
405
+
406
+ visit spree.admin_path
407
+
408
+ expect(page).to have_selector("#listing_orders tbody tr", count: 2)
409
+
410
+ click_link order.number
411
+
412
+ click_link "Payments"
413
+ find(".fa-capture").click
380
414
 
381
- within_3d_secure_modal do
382
- click_button 'Complete authentication'
415
+ expect(page).to have_content "Payment Updated"
416
+ expect(find("table#payments")).to have_content "Completed"
417
+
418
+ # Order cancel, after capture
419
+ click_link "Cart"
420
+
421
+ within "#sidebar" do
422
+ expect(page).to have_content "Completed"
423
+ end
424
+
425
+ find('input[value="Cancel"]').click
426
+
427
+ expect(page).to have_content "Order canceled"
428
+
429
+ within "#sidebar" do
430
+ expect(page).to have_content "Canceled"
431
+ end
432
+ end
383
433
  end
434
+ end
384
435
 
385
- expect(page).to have_current_path("/checkout/confirm")
386
- click_button "Place Order"
387
- expect(page).to have_content("Your order has been processed successfully")
436
+ context "when paying with multiple payment methods" do
437
+ stub_authorization!
388
438
 
389
- visit spree.root_path
390
- click_link "DL-44"
391
- click_button "Add To Cart"
439
+ context "when paying first with regular card, then with 3D-Secure card" do
440
+ let(:regular_card) { "4242 4242 4242 4242"}
392
441
 
393
- expect(page).to have_current_path("/cart")
394
- click_button "Checkout"
442
+ it "voids the first stripe payment and successfully pays with 3DS card" do
443
+ within_frame find('#card_number iframe') do
444
+ regular_card.split('').each { |n| find_field('cardnumber').native.send_keys(n) }
445
+ end
446
+ within_frame(find '#card_cvc iframe') { fill_in 'cvc', with: '123' }
447
+ within_frame(find '#card_expiry iframe') do
448
+ '0132'.split('').each { |n| find_field('exp-date').native.send_keys(n) }
449
+ end
450
+ click_button "Save and Continue"
395
451
 
396
- # Address
397
- expect(page).to have_current_path("/checkout/address")
452
+ expect(page).to have_content "Ending in 4242"
398
453
 
399
- within("#billing") do
400
- fill_in_name
401
- fill_in "Street Address", with: "YT-1300"
402
- fill_in "City", with: "Mos Eisley"
403
- select "United States of America", from: "Country"
404
- select country.states.first.name, from: "order_bill_address_attributes_state_id"
405
- fill_in "Zip", with: "12010"
406
- fill_in "Phone", with: "(555) 555-5555"
454
+ click_link "Payment"
455
+
456
+ authenticate_3d_secure_card(card_3d_secure)
457
+ click_button "Place Order"
458
+ expect(page).to have_content "Your order has been processed successfully"
459
+
460
+ visit spree.admin_path
461
+ click_link Spree::Order.complete.first.number
462
+ click_link "Payments"
463
+
464
+ payments = all('table#payments tbody tr')
465
+
466
+ expect(payments.first).to have_content "Stripe"
467
+ expect(payments.first).to have_content "Void"
468
+
469
+ expect(payments.last).to have_content "Stripe"
470
+ expect(payments.last).to have_content "Pending"
471
+ end
407
472
  end
408
- click_on "Save and Continue"
409
473
 
410
- # Delivery
411
- expect(page).to have_current_path("/checkout/delivery")
412
- expect(page).to have_content("UPS Ground")
413
- click_on "Save and Continue"
474
+ context "when paying first with 3D-Secure card, then with check" do
475
+ before { create :check_payment_method }
414
476
 
415
- # Payment
416
- expect(page).to have_current_path("/checkout/payment")
417
- choose "Use an existing card on file"
418
- click_button "Save and Continue"
477
+ it "voids the stripe payment and successfully pays with check" do
478
+ authenticate_3d_secure_card(card_3d_secure)
479
+ expect(page).to have_current_path("/checkout/confirm")
419
480
 
420
- # Confirm
421
- expect(page).to have_current_path("/checkout/confirm")
422
- click_button "Place Order"
423
- expect(page).to have_content("Your order has been processed successfully")
481
+ click_link "Payment"
482
+ choose "Check"
483
+ click_button "Save and Continue"
484
+ expect(find(".payment-info")).to have_content "Check"
485
+ expect(page).to have_content "Your order has been processed successfully"
486
+
487
+ visit spree.admin_path
488
+ click_link Spree::Order.complete.first.number
489
+ click_link "Payments"
490
+ payments = all('table#payments tbody tr')
491
+
492
+ stripe_payment = payments.first
493
+ expect(stripe_payment).to have_content "Stripe"
494
+ expect(stripe_payment).to have_content "Void"
495
+
496
+ check_payment = payments.last
497
+ expect(check_payment).to have_content "Check"
498
+ end
499
+ end
424
500
  end
425
501
 
426
502
  it_behaves_like "Stripe Elements invalid payments"
427
503
  end
428
504
 
429
505
  def within_3d_secure_modal
430
- within_frame "__privateStripeFrame10" do
431
- within_frame "challengeFrame" do
432
- yield
506
+ within_frame "__privateStripeFrame11" do
507
+ within_frame "__stripeJSChallengeFrame" do
508
+ within_frame "acsFrame" do
509
+ yield
510
+ end
433
511
  end
434
512
  end
435
513
  end
514
+
515
+ def authenticate_3d_secure_card(card_number)
516
+ within_frame find('#card_number iframe') do
517
+ card_number.split('').each { |n| find_field('cardnumber').native.send_keys(n) }
518
+ end
519
+ within_frame(find '#card_cvc iframe') { fill_in 'cvc', with: '123' }
520
+ within_frame(find '#card_expiry iframe') do
521
+ '0132'.split('').each { |n| find_field('exp-date').native.send_keys(n) }
522
+ end
523
+ click_button "Save and Continue"
524
+
525
+ within_3d_secure_modal do
526
+ expect(page).to have_content '$19.99 using 3D Secure'
527
+
528
+ click_button 'Complete authentication'
529
+ end
530
+ end
436
531
  end