solidus_stripe 3.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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