diff --git a/app/controllers/concerns/shopify_app/authenticated.rb b/app/controllers/concerns/shopify_app/authenticated.rb index 8ea02a5a2..fba8fbb95 100644 --- a/app/controllers/concerns/shopify_app/authenticated.rb +++ b/app/controllers/concerns/shopify_app/authenticated.rb @@ -9,6 +9,8 @@ module Authenticated include ShopifyApp::LoginProtection include ShopifyApp::CsrfProtection include ShopifyApp::EmbeddedApp + include ShopifyApp::EnsureBilling + before_action :login_again_if_different_user_or_shop around_action :activate_shopify_session after_action :add_top_level_redirection_headers diff --git a/app/controllers/shopify_app/callback_controller.rb b/app/controllers/shopify_app/callback_controller.rb index 5867b3092..923f1fd4b 100644 --- a/app/controllers/shopify_app/callback_controller.rb +++ b/app/controllers/shopify_app/callback_controller.rb @@ -4,6 +4,7 @@ module ShopifyApp # Performs login after OAuth completes class CallbackController < ActionController::Base include ShopifyApp::LoginProtection + include ShopifyApp::EnsureBilling def callback begin @@ -34,8 +35,9 @@ def callback end perform_post_authenticate_jobs(auth_result[:session]) + has_payment = check_billing(auth_result[:session]) - respond_successfully + respond_successfully if has_payment end private diff --git a/lib/generators/shopify_app/install/templates/shopify_app.rb.tt b/lib/generators/shopify_app/install/templates/shopify_app.rb.tt index 91e43045b..1539420b1 100644 --- a/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +++ b/lib/generators/shopify_app/install/templates/shopify_app.rb.tt @@ -13,6 +13,19 @@ ShopifyApp.configure do |config| config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence + # You may want to charge merchants for using your app. Setting the billing configuration will cause the Authenticated + # controller concern to check that the session is for a merchant that has an active one-time payment or subscription. + # If no payment is found, it starts off the process and sends the merchant to a confirmation URL so that they can + # approve the purchase. + # + # Learn more about billing in our documentation: https://shopify.dev/apps/billing + # config.billing = ShopifyApp::BillingConfiguration.new( + # charge_name: "My app billing charge", + # amount: 5, + # interval: ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS, + # currency_code: "USD", # Only supports USD for now + # ) + if defined? Rails::Server raise('Missing SHOPIFY_API_KEY. See https://github.com/Shopify/shopify_app#requirements') unless config.api_key raise('Missing SHOPIFY_API_SECRET. See https://github.com/Shopify/shopify_app#requirements') unless config.secret diff --git a/lib/shopify_app.rb b/lib/shopify_app.rb index 66c9f5c8b..5411d794c 100644 --- a/lib/shopify_app.rb +++ b/lib/shopify_app.rb @@ -39,6 +39,7 @@ def self.use_webpacker? require "shopify_app/controller_concerns/localization" require "shopify_app/controller_concerns/itp" require "shopify_app/controller_concerns/login_protection" + require "shopify_app/controller_concerns/ensure_billing" require "shopify_app/controller_concerns/embedded_app" require "shopify_app/controller_concerns/payload_verification" require "shopify_app/controller_concerns/app_proxy_verification" diff --git a/lib/shopify_app/configuration.rb b/lib/shopify_app/configuration.rb index a8e983eda..23debe03d 100644 --- a/lib/shopify_app/configuration.rb +++ b/lib/shopify_app/configuration.rb @@ -39,6 +39,9 @@ class Configuration # allow namespacing webhook jobs attr_accessor :webhook_jobs_namespace + # takes a ShopifyApp::BillingConfiguration object + attr_accessor :billing + def initialize @root_url = "/" @myshopify_domain = "myshopify.com" @@ -90,6 +93,10 @@ def has_scripttags? scripttags.present? end + def requires_billing? + billing.present? + end + def shop_access_scopes @shop_access_scopes || scope end @@ -99,6 +106,24 @@ def user_access_scopes end end + class BillingConfiguration + INTERVAL_ONE_TIME = "ONE_TIME" + INTERVAL_EVERY_30_DAYS = "EVERY_30_DAYS" + INTERVAL_ANNUAL = "ANNUAL" + + attr_reader :charge_name + attr_reader :amount + attr_reader :currency_code + attr_reader :interval + + def initialize(charge_name:, amount:, interval:, currency_code: "USD") + @charge_name = charge_name + @amount = amount + @currency_code = currency_code + @interval = interval + end + end + def self.configuration @configuration ||= Configuration.new end diff --git a/lib/shopify_app/controller_concerns/ensure_billing.rb b/lib/shopify_app/controller_concerns/ensure_billing.rb new file mode 100644 index 000000000..46320964a --- /dev/null +++ b/lib/shopify_app/controller_concerns/ensure_billing.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +module ShopifyApp + module EnsureBilling + class BillingError < StandardError + attr_accessor :message + attr_accessor :errors + + def initialize(message, errors) + super + @message = message + @errors = errors + end + end + + extend ActiveSupport::Concern + + RECURRING_INTERVALS = [BillingConfiguration::INTERVAL_EVERY_30_DAYS, BillingConfiguration::INTERVAL_ANNUAL] + + included do + before_action :check_billing, if: :billing_required? + rescue_from BillingError, with: :handle_billing_error + end + + private + + def check_billing(session = current_shopify_session) + return true if session.blank? || !billing_required? + + confirmation_url = nil + + if has_active_payment?(session) + has_payment = true + else + has_payment = false + confirmation_url = request_payment(session) + end + + unless has_payment + if request.xhr? + add_top_level_redirection_headers(url: confirmation_url, ignore_response_code: true) + head(:unauthorized) + else + redirect_to(confirmation_url, allow_other_host: true) + end + end + + has_payment + end + + def billing_required? + ShopifyApp.configuration.requires_billing? + end + + def handle_billing_error(error) + logger.info("#{error.message}: #{error.errors}") + redirect_to_login + end + + def has_active_payment?(session) + if recurring? + has_subscription?(session) + else + has_one_time_payment?(session) + end + end + + def has_subscription?(session) + response = run_query(session: session, query: RECURRING_PURCHASES_QUERY) + subscriptions = response.body["data"]["currentAppInstallation"]["activeSubscriptions"] + + subscriptions.each do |subscription| + if subscription["name"] == ShopifyApp.configuration.billing.charge_name && + (!Rails.env.production? || !subscription["test"]) + + return true + end + end + + false + end + + def has_one_time_payment?(session) + purchases = nil + end_cursor = nil + + loop do + response = run_query(session: session, query: ONE_TIME_PURCHASES_QUERY, variables: { endCursor: end_cursor }) + purchases = response.body["data"]["currentAppInstallation"]["oneTimePurchases"] + + purchases["edges"].each do |purchase| + node = purchase["node"] + + if node["name"] == ShopifyApp.configuration.billing.charge_name && + (!Rails.env.production? || !node["test"]) && + node["status"] == "ACTIVE" + + return true + end + end + + end_cursor = purchases["pageInfo"]["endCursor"] + break unless purchases["pageInfo"]["hasNextPage"] + end + + false + end + + def request_payment(session) + shop = session.shop + host = Base64.encode64("#{shop}/admin") + return_url = "https://#{ShopifyAPI::Context.host_name}?shop=#{shop}&host=#{host}" + + if recurring? + data = request_recurring_payment(session: session, return_url: return_url) + data = data["data"]["appSubscriptionCreate"] + else + data = request_one_time_payment(session: session, return_url: return_url) + data = data["data"]["appPurchaseOneTimeCreate"] + end + + raise BillingError.new("Error while billing the store", data["userErrros"]) unless data["userErrors"].empty? + + data["confirmationUrl"] + end + + def request_recurring_payment(session:, return_url:) + response = run_query( + session: session, + query: RECURRING_PURCHASE_MUTATION, + variables: { + name: ShopifyApp.configuration.billing.charge_name, + lineItems: { + plan: { + appRecurringPricingDetails: { + interval: ShopifyApp.configuration.billing.interval, + price: { + amount: ShopifyApp.configuration.billing.amount, + currencyCode: ShopifyApp.configuration.billing.currency_code, + }, + }, + }, + }, + returnUrl: return_url, + test: !Rails.env.production?, + } + ) + + response.body + end + + def request_one_time_payment(session:, return_url:) + response = run_query( + session: session, + query: ONE_TIME_PURCHASE_MUTATION, + variables: { + name: ShopifyApp.configuration.billing.charge_name, + price: { + amount: ShopifyApp.configuration.billing.amount, + currencyCode: ShopifyApp.configuration.billing.currency_code, + }, + returnUrl: return_url, + test: !Rails.env.production?, + } + ) + + response.body + end + + def recurring? + RECURRING_INTERVALS.include?(ShopifyApp.configuration.billing.interval) + end + + def run_query(session:, query:, variables: nil) + client = ShopifyAPI::Clients::Graphql::Admin.new(session: session) + + response = client.query(query: query, variables: variables) + + raise BillingError.new("Error while billing the store", []) unless response.ok? + raise BillingError.new("Error while billing the store", response.body["errors"]) if response.body["errors"] + + response + end + + RECURRING_PURCHASES_QUERY = <<~'QUERY' + query appSubscription { + currentAppInstallation { + activeSubscriptions { + name, test + } + } + } + QUERY + + ONE_TIME_PURCHASES_QUERY = <<~'QUERY' + query appPurchases($endCursor: String) { + currentAppInstallation { + oneTimePurchases(first: 250, sortKey: CREATED_AT, after: $endCursor) { + edges { + node { + name, test, status + } + } + pageInfo { + hasNextPage, endCursor + } + } + } + } + QUERY + + RECURRING_PURCHASE_MUTATION = <<~'QUERY' + mutation createPaymentMutation( + $name: String! + $lineItems: [AppSubscriptionLineItemInput!]! + $returnUrl: URL! + $test: Boolean + ) { + appSubscriptionCreate( + name: $name + lineItems: $lineItems + returnUrl: $returnUrl + test: $test + ) { + confirmationUrl + userErrors { + field, message + } + } + } + QUERY + + ONE_TIME_PURCHASE_MUTATION = <<~'QUERY' + mutation createPaymentMutation( + $name: String! + $price: MoneyInput! + $returnUrl: URL! + $test: Boolean + ) { + appPurchaseOneTimeCreate( + name: $name + price: $price + returnUrl: $returnUrl + test: $test + ) { + confirmationUrl + userErrors { + field, message + } + } + } + QUERY + end +end diff --git a/lib/shopify_app/controller_concerns/login_protection.rb b/lib/shopify_app/controller_concerns/login_protection.rb index 4ee7b62de..28b229b2c 100644 --- a/lib/shopify_app/controller_concerns/login_protection.rb +++ b/lib/shopify_app/controller_concerns/login_protection.rb @@ -70,7 +70,7 @@ def jwt_expire_at expire_at - 5.seconds # 5s gap to start fetching new token in advance end - def add_top_level_redirection_headers(ignore_response_code: false) + def add_top_level_redirection_headers(url: nil, ignore_response_code: false) if request.xhr? && (ignore_response_code || response.code.to_i == 401) # Make sure the shop is set in the redirection URL unless params[:shop] @@ -82,8 +82,10 @@ def add_top_level_redirection_headers(ignore_response_code: false) end end + url ||= login_url_with_optional_shop + response.set_header("X-Shopify-API-Request-Failure-Reauthorize", "1") - response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", login_url_with_optional_shop) + response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url) end end diff --git a/test/controllers/concerns/authenticated_test.rb b/test/controllers/concerns/authenticated_test.rb index 503b7ea42..c12c6cbd0 100644 --- a/test/controllers/concerns/authenticated_test.rb +++ b/test/controllers/concerns/authenticated_test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "test_helper" + class AuthenticatedTest < ActionController::TestCase class AuthenticatedTestController < ActionController::Base include ShopifyApp::Authenticated @@ -15,5 +17,6 @@ def index AuthenticatedTestController.include?(ShopifyApp::LoginProtection) AuthenticatedTestController.include?(ShopifyApp::CsrfProtection) AuthenticatedTestController.include?(ShopifyApp::EmbeddedApp) + AuthenticatedTestController.include?(ShopifyApp::EnsureBilling) end end diff --git a/test/controllers/concerns/ensure_billing_test.rb b/test/controllers/concerns/ensure_billing_test.rb new file mode 100644 index 000000000..be5e7d883 --- /dev/null +++ b/test/controllers/concerns/ensure_billing_test.rb @@ -0,0 +1,351 @@ +# frozen_string_literal: true + +require "test_helper" +require "action_controller" +require "action_controller/base" + +class EnsureBillingTest < ActionController::TestCase + class BillingTestController < ActionController::Base + include ShopifyApp::LoginProtection + include ShopifyApp::EnsureBilling + + def index + render(html: "

Success") + end + end + + tests BillingTestController + + SHOP = "my-shop.myshopify.com" + TEST_CHARGE_NAME = "Test charge" + + setup do + Rails.application.routes.draw do + get "/billing", to: "ensure_billing_test/billing_test#index" + end + + ShopifyApp::SessionRepository.shop_storage = ShopifyApp::InMemoryShopSessionStore + ShopifyApp::SessionRepository.user_storage = ShopifyApp::InMemoryUserSessionStore + + @session = ShopifyAPI::Auth::Session.new( + id: "1234", + shop: SHOP, + access_token: "access-token", + scope: ["read_products"], + ) + ShopifyAPI::Utils::SessionUtils.stubs(:load_current_session).returns(@session) + + @api_version = ShopifyAPI::LATEST_SUPPORTED_ADMIN_VERSION + + ShopifyAPI::Context.setup( + api_key: "api_key", + api_secret_key: "api_secret_key", + api_version: @api_version, + host_name: "host.example.io", + scope: "read_products", + session_storage: ShopifyApp::SessionRepository, + is_private: false, + is_embedded: true, + ) + end + + test "requires single payment if none exists and non recurring" do + ShopifyApp.configuration.billing = ShopifyApp::BillingConfiguration.new( + charge_name: TEST_CHARGE_NAME, + amount: 5, + interval: ShopifyApp::BillingConfiguration::INTERVAL_ONE_TIME, + ) + stub_graphql_requests( + { request_body: /oneTimePurchases/, response_body: EMPTY_SUBSCRIPTIONS }, + { + request_body: hash_including({ + query: /appPurchaseOneTimeCreate/, + variables: hash_including({ name: TEST_CHARGE_NAME }), + }), + response_body: PURCHASE_ONE_TIME_RESPONSE, + }, + ) + + get :index + + assert_redirected_to(%r{^https://totally-real-url}) + + get :index, xhr: true + + assert_response :unauthorized + assert_match "1", response.headers["X-Shopify-API-Request-Failure-Reauthorize"] + assert_match(%r{^https://totally-real-url}, response.headers["X-Shopify-API-Request-Failure-Reauthorize-Url"]) + end + + test "requires subscription if none exists and recurring" do + ShopifyApp.configuration.billing = ShopifyApp::BillingConfiguration.new( + charge_name: TEST_CHARGE_NAME, + amount: 5, + interval: ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS, + ) + stub_graphql_requests( + { request_body: /activeSubscriptions/, response_body: EMPTY_SUBSCRIPTIONS }, + { + request_body: hash_including({ + query: /appSubscriptionCreate/, + variables: hash_including({ + name: TEST_CHARGE_NAME, + lineItems: { + plan: { + appRecurringPricingDetails: hash_including({ + interval: ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS, + }), + }, + }, + }), + }), + response_body: PURCHASE_SUBSCRIPTION_RESPONSE, + }, + ) + + get :index + + assert_redirected_to(%r{^https://totally-real-url}) + + get :index, xhr: true + + assert_response :unauthorized + assert_match "1", response.headers["X-Shopify-API-Request-Failure-Reauthorize"] + assert_match(%r{^https://totally-real-url}, response.headers["X-Shopify-API-Request-Failure-Reauthorize-Url"]) + end + + test "does not require single payment if exists and non recurring" do + ShopifyApp.configuration.billing = ShopifyApp::BillingConfiguration.new( + charge_name: TEST_CHARGE_NAME, + amount: 5, + interval: ShopifyApp::BillingConfiguration::INTERVAL_ONE_TIME, + ) + stub_graphql_requests({ request_body: /oneTimePurchases/, response_body: EXISTING_ONE_TIME_PAYMENT }) + + get :index + + assert_response :success + + get :index, xhr: true + + assert_response :success + refute response.headers["X-Shopify-API-Request-Failure-Reauthorize"].present? + refute response.headers["X-Shopify-API-Request-Failure-Reauthorize-Url"].present? + end + + test "does not require subscription if exists and recurring" do + ShopifyApp.configuration.billing = ShopifyApp::BillingConfiguration.new( + charge_name: TEST_CHARGE_NAME, + amount: 5, + interval: ShopifyApp::BillingConfiguration::INTERVAL_ANNUAL, + ) + stub_graphql_requests({ request_body: /activeSubscriptions/, response_body: EXISTING_SUBSCRIPTION }) + + get :index + + assert_response :success + + get :index, xhr: true + + assert_response :success + refute response.headers["X-Shopify-API-Request-Failure-Reauthorize"].present? + refute response.headers["X-Shopify-API-Request-Failure-Reauthorize-Url"].present? + end + + test "ignores non active one time payments" do + ShopifyApp.configuration.billing = ShopifyApp::BillingConfiguration.new( + charge_name: TEST_CHARGE_NAME, + amount: 5, + interval: ShopifyApp::BillingConfiguration::INTERVAL_ONE_TIME, + ) + stub_graphql_requests( + { request_body: /oneTimePurchases/, response_body: EXISTING_INACTIVE_ONE_TIME_PAYMENT }, + { + request_body: hash_including({ + query: /appPurchaseOneTimeCreate/, + variables: hash_including({ name: TEST_CHARGE_NAME }), + }), + response_body: PURCHASE_ONE_TIME_RESPONSE, + }, + ) + + get :index + + assert_redirected_to(%r{^https://totally-real-url}) + + get :index, xhr: true + + assert_response :unauthorized + assert_match "1", response.headers["X-Shopify-API-Request-Failure-Reauthorize"] + assert_match(%r{^https://totally-real-url}, response.headers["X-Shopify-API-Request-Failure-Reauthorize-Url"]) + end + + test "paginates until a payment is found" do + ShopifyApp.configuration.billing = ShopifyApp::BillingConfiguration.new( + charge_name: TEST_CHARGE_NAME, + amount: 5, + interval: ShopifyApp::BillingConfiguration::INTERVAL_ONE_TIME, + ) + stub_graphql_requests( + { + request_body: hash_including({ + query: /oneTimePurchases/, + variables: { endCursor: nil }, + }), + response_body: EXISTING_ONE_TIME_PAYMENT_WITH_PAGINATION[0], + }, + { + request_body: hash_including({ + query: /oneTimePurchases/, + variables: { endCursor: "end_cursor" }, + }), + response_body: EXISTING_ONE_TIME_PAYMENT_WITH_PAGINATION[1], + }, + ) + + get :index + + assert_response :success + + get :index, xhr: true + + assert_response :success + refute response.headers["X-Shopify-API-Request-Failure-Reauthorize"].present? + refute response.headers["X-Shopify-API-Request-Failure-Reauthorize-Url"].present? + end + + private + + def stub_graphql_requests(*requests) + requests.each do |request| + stub_request(:post, "https://my-shop.myshopify.com/admin/api/#{@api_version}/graphql.json") + .with( + body: request[:request_body], + headers: { "X-Shopify-Access-Token": "access-token" } + ) + .to_return( + status: 200, + body: JSON.dump(request[:response_body]), + ) + end + end + + EMPTY_SUBSCRIPTIONS = { + data: { + currentAppInstallation: { + oneTimePurchases: { + edges: [], + pageInfo: { hasNextPage: false, endCursor: nil }, + }, + activeSubscriptions: [], + userErrors: {}, + }, + }, + } + + EXISTING_ONE_TIME_PAYMENT = { + data: { + currentAppInstallation: { + oneTimePurchases: { + edges: [ + { + node: { + name: TEST_CHARGE_NAME, + test: true, status: "ACTIVE", + }, + }, + ], + pageInfo: { hasNextPage: false, endCursor: nil }, + }, + activeSubscriptions: [], + }, + }, + } + + EXISTING_ONE_TIME_PAYMENT_WITH_PAGINATION = [ + { + data: { + currentAppInstallation: { + oneTimePurchases: { + edges: [ + { + node: { name: "some_other_name", test: true, status: "ACTIVE" }, + }, + ], + pageInfo: { hasNextPage: true, endCursor: "end_cursor" }, + }, + activeSubscriptions: [], + }, + }, + }, + { + data: { + currentAppInstallation: { + oneTimePurchases: { + edges: [ + { + node: { + name: TEST_CHARGE_NAME, + test: true, + status: "ACTIVE", + }, + }, + ], + pageInfo: { hasNextPage: false, endCursor: nil }, + }, + activeSubscriptions: [], + }, + }, + }, + ] + + EXISTING_INACTIVE_ONE_TIME_PAYMENT = { + data: { + currentAppInstallation: { + oneTimePurchases: { + edges: [ + { + node: { + name: TEST_CHARGE_NAME, + test: true, + status: "PENDING", + }, + }, + ], + pageInfo: { hasNextPage: false, endCursor: nil }, + }, + activeSubscriptions: [], + }, + }, + } + + EXISTING_SUBSCRIPTION = { + data: { + currentAppInstallation: { + oneTimePurchases: { + edges: [], + pageInfo: { hasNextPage: false, endCursor: nil }, + }, + activeSubscriptions: [{ name: TEST_CHARGE_NAME, test: true }], + }, + }, + } + + PURCHASE_ONE_TIME_RESPONSE = { + data: { + appPurchaseOneTimeCreate: { + confirmationUrl: "https://totally-real-url", + userErrors: {}, + }, + }, + } + + PURCHASE_SUBSCRIPTION_RESPONSE = { + data: { + appSubscriptionCreate: { + confirmationUrl: "https://totally-real-url", + userErrors: {}, + }, + }, + } +end diff --git a/test/controllers/concerns/require_known_shop_test.rb b/test/controllers/concerns/require_known_shop_test.rb index c3097bca6..9347c4fdc 100644 --- a/test/controllers/concerns/require_known_shop_test.rb +++ b/test/controllers/concerns/require_known_shop_test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "test_helper" + class RequireKnownShopTest < ActionController::TestCase class UnauthenticatedTestController < ActionController::Base include ShopifyApp::RequireKnownShop diff --git a/test/dummy/config/initializers/shopify_app.rb b/test/dummy/config/initializers/shopify_app.rb index 53967aa4d..98992071b 100644 --- a/test/dummy/config/initializers/shopify_app.rb +++ b/test/dummy/config/initializers/shopify_app.rb @@ -12,6 +12,7 @@ def self.call config.embedded_app = true config.myshopify_domain = "myshopify.com" config.api_version = :unstable + config.billing = nil config.shop_session_repository = ShopifyApp::InMemorySessionStore config.after_authenticate_job = false diff --git a/test/shopify_app/controller_concerns/csrf_protection_test.rb b/test/shopify_app/controller_concerns/csrf_protection_test.rb index ccceeb1eb..8a5712408 100644 --- a/test/shopify_app/controller_concerns/csrf_protection_test.rb +++ b/test/shopify_app/controller_concerns/csrf_protection_test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "test_helper" + class CsrfProtectionController < ActionController::Base include ShopifyApp::LoginProtection include ShopifyApp::CsrfProtection diff --git a/test/utils/rails_generator_runtime.rb b/test/utils/rails_generator_runtime.rb index f46e7a234..e432f3726 100644 --- a/test/utils/rails_generator_runtime.rb +++ b/test/utils/rails_generator_runtime.rb @@ -75,7 +75,6 @@ class << self @initialized = false def with_session(test_class, is_embedded: false, is_private: false, &block) - WebMock.enable! original_embedded_app = ShopifyApp.configuration.embedded_app ShopifyApp.configuration.embedded_app = false unless is_embedded ShopifyAPI::Context.setup( @@ -94,8 +93,6 @@ def with_session(test_class, is_embedded: false, is_private: false, &block) runtime = Utils::RailsGeneratorRuntime.new(test_class) block.call(runtime) ensure - WebMock.reset! - WebMock.disable! ShopifyApp.configuration.embedded_app = original_embedded_app ShopifyAPI::Context.deactivate_session runtime&.clear