Hey @KyleG-Shopify,
The second screenshot is our alter bot when billing fails.
The try again button goes back to the App Pricing hosted page.
The merchant did the full billing flow 4 times, however, there are no updates in the partner dashboard.
A bit of history, the merchant had a previous charge (created with old Billing API about 2 years ago). He cancelled that plan and tried again after we migrated to Shopify App Pricing. We got one install in the meantime (from another merchant) and it worked, but for this merchant it doesn’t work. I guess I will have to try again when his old billing period ends.
Below is the /welcome method and fetch_subscription_status method
<s-button variant="primary" href="<%= hosted_pricing_url(@shop_origin) %>" target="_top">
Try again (Shopify App Pricing)
</s-button>
def hosted_pricing_url(shop_domain)
store_handle = shop_store_handle(shop_domain)
app_handle = ENV.fetch("SHOPIFY_APP_HANDLE")
"https://admin.shopify.com/store/#{store_handle}/charges/#{app_handle}/pricing_plans"
end
def welcome
Rails.logger.info "Billing#welcome params: #{params.inspect}"
@shop_domain = params[:shop] || @shop_origin
@plan_handle = params[:plan_handle]
unless @plan_handle.present?
Rails.logger.warn("Billing#welcome called without plan_handle for shop=#{@shop_domain}")
flash.now[:error] = "We couldn't determine your plan. Please contact support."
return render :welcome
end
internal_plan = case @plan_handle
when "shopify-test" then "growth"
else @plan_handle
end
sub_status = current_shop.fetch_subscription_status
if sub_status[:source] == :none
Rails.logger.warn(
"Billing#welcome: No active subscription found for shop=#{@shop_domain} " \
"plan_handle=#{@plan_handle}. Blocking activation."
)
begin
message = "⚠️ BILLING ERROR: #{@shop_domain} hit /welcome with plan_handle=#{@plan_handle} " \
"but NO active subscription found on Shopify. Activation BLOCKED.\n" \
"Error Details: #{sub_status[:error] || 'None'}"
SuperspeedRoom.new(message: message).send
rescue => e
Rails.logger.error("Slack alert failed: #{e.message}")
end
@subscription_error = true
return render :welcome
end
Rails.logger.info(
"Billing#welcome: Subscription verified (#{sub_status[:source]}) for shop=#{@shop_domain} " \
"name=#{sub_status[:name]} price=#{sub_status[:price]}"
)
current_shop.update(billing_flag: nil) if current_shop.respond_to?(:billing_flag)
enable_store_from_plan(internal_plan)
@internal_plan = internal_plan
@page_limit = current_shop.page_limit
end
# frozen_string_literal: true
module SubscriptionStatus
extend ActiveSupport::Concern
# Returns a hash with subscription info from the GraphQL API:
# { source: :graphql | :none, name:, price:, status:, billing_on:, trial_days:, raw: }
def fetch_subscription_status
graphql_sub = fetch_graphql_subscription
if graphql_sub
return {
source: :graphql,
id: graphql_sub["id"],
name: graphql_sub["name"],
price: graphql_sub.dig("lineItems", 0, "plan", "pricingDetails", "price", "amount"),
status: graphql_sub["status"],
billing_on: graphql_sub["currentPeriodEnd"],
trial_days: nil,
trial_ends_on: graphql_sub["trialDays"],
activated_on: graphql_sub["createdAt"],
raw: graphql_sub
}
end
# No subscription found
{ source: :none }
rescue => e
Rails.logger.error("fetch_subscription_status error for #{shopify_domain}: #{e.class} - #{e.message}")
{ source: :none, error: e.message }
end
private
def fetch_graphql_subscription
result = wss do
client = ShopifyAPI::Clients::Graphql::Admin.new(session: ShopifyAPI::Context.active_session)
client.query(query: ACTIVE_SUBSCRIPTIONS_QUERY)
end
subs = result&.body&.dig("data", "currentAppInstallation", "activeSubscriptions")
subs&.first
rescue => e
Rails.logger.error("fetch_graphql_subscription error for #{shopify_domain}: #{e.class} - #{e.message}")
nil
end
ACTIVE_SUBSCRIPTIONS_QUERY = <<~GQL
{
currentAppInstallation {
activeSubscriptions {
id
name
status
createdAt
currentPeriodEnd
trialDays
test
lineItems {
id
plan {
pricingDetails {
__typename
... on AppRecurringPricing {
price { amount currencyCode }
interval
}
... on AppUsagePricing {
balanceUsed { amount currencyCode }
cappedAmount { amount currencyCode }
}
}
}
}
}
}
}
GQL
end
Thank you very much for your time!