defmodule ShopifexWeb.PaymentController do @moduledoc """ You can use this module inside of another controller to handle initial iFrame load and shop installation Example: mix phx.gen.html Shops Plan plans name:string price:string features:array grants:array test:boolean mix phx.gen.html Shops Grant grants shop:references:shops charge_id:integer grants:array ```elixir defmodule MyAppWeb.PaymentController do use MyAppWeb, :controller use ShopifexWeb.PaymentController # Thats it! You can now configure your purchasable products :) end ``` """ @doc """ Display the available payment plans for the user to select. """ @callback render_plans( conn :: Plug.Conn.t(), guard_identifier :: String.t(), redirect_after :: String.t() ) :: Plug.Conn.t() @doc """ An optional callback called after a payment is completed. By default, this function redirects the user to the app index within their Shopify admin panel. ## Example def after_payment(conn, shop, plan, grant, redirect_after) do # send yourself an e-mail about payment # follow default behaviour. super(conn, shop, plan, grant, redirect_after) end """ @callback after_payment( Plug.Conn.t(), Ecto.Schema.t(), Ecto.Schema.t(), Ecto.Schema.t(), String.t() ) :: Plug.Conn.t() @doc """ Given a shop and plan, return boolean for whether the charge should be created as a test charge. """ @callback test_charge?(shop :: Ecto.Schema.t(), plan :: map()) :: boolean() @optional_callbacks render_plans: 3, after_payment: 5, test_charge?: 2 defmacro __using__(_opts) do quote do @behaviour ShopifexWeb.PaymentController require Logger def show_plans(conn, params) do payment_guard = Application.fetch_env!(:shopifex, :payment_guard) path_prefix = Application.get_env(:shopifex, :path_prefix, "") default_redirect_after = path_prefix <> "/?token=" <> Guardian.Plug.current_token(conn) render_plans( conn, Map.get(params, "guard_identifier"), Map.get(params, "redirect_after", default_redirect_after) ) end def select_plan(conn, %{"plan_id" => plan_id, "redirect_after" => redirect_after}) do payment_guard = Application.fetch_env!(:shopifex, :payment_guard) redirect_after_agent = Application.get_env(:shopifex, :redirect_after_agent, Shopifex.RedirectAfterAgent) plan = payment_guard.get_plan(plan_id) shop = case conn.private do %{shop: shop} -> shop %{guardian_default_resource: shop} -> shop end {:ok, charge} = create_charge(shop, plan) redirect_after_agent.set(charge["id"], redirect_after) send_resp(conn, 200, Jason.encode!(charge)) end @impl ShopifexWeb.PaymentController def test_charge?(_shop, %{test: test?} = _plan), do: test? # annual billing is only possible w/ the GraphQL API defp create_charge(shop, plan = %{type: "recurring_application_charge", annual: true}) do redirect_uri = Application.get_env(:shopifex, :payment_redirect_uri) case Neuron.query( """ mutation appSubscriptionCreate($name: String!, $return_url: URL!, $test: Boolean!, $price: Decimal!, $trial_days: Int!) { appSubscriptionCreate(name: $name, returnUrl: $return_url, test: $test, trialDays: $trial_days, lineItems: [{plan: {appRecurringPricingDetails: {price: {amount: $price, currencyCode: USD}, interval: ANNUAL}}}]) { appSubscription { id } confirmationUrl userErrors { field message } } } """, %{ name: plan.name, price: plan.price, test: test_charge?(shop, plan), trial_days: Map.get(plan, :trial_days, 0), return_url: "#{redirect_uri}?plan_id=#{plan.id}&shop=#{Shopifex.Shops.get_url(shop)}" }, url: "https://#{Shopifex.Shops.get_url(shop)}/admin/api/2024-01/graphql.json", headers: [ "X-Shopify-Access-Token": shop.access_token, "Content-Type": "application/json" ] ) do {:ok, %Neuron.Response{body: body}} -> app_subscription_create = body["data"]["appSubscriptionCreate"] # id comes back in this format: "gid://shopify/AppSubscription/4019552312" <<_::binary-size(30)>> <> id = app_subscription_create["appSubscription"]["id"] confirmation_url = app_subscription_create["confirmationUrl"] {:ok, %{"id" => id, "confirmation_url" => confirmation_url}} end end defp create_charge(shop, plan = %{type: "recurring_application_charge"}) do redirect_uri = Application.get_env(:shopifex, :payment_redirect_uri) body = Jason.encode!(%{ recurring_application_charge: %{ name: plan.name, price: plan.price, test: test_charge?(shop, plan), trial_days: Map.get(plan, :trial_days, 0), return_url: "#{redirect_uri}?plan_id=#{plan.id}&shop=#{Shopifex.Shops.get_url(shop)}" } }) case HTTPoison.post( "https://#{Shopifex.Shops.get_url(shop)}/admin/api/2024-01/recurring_application_charges.json", body, "X-Shopify-Access-Token": shop.access_token, "Content-Type": "application/json" ) do {:ok, resp} -> {:ok, Jason.decode!(resp.body)["recurring_application_charge"]} end end defp create_charge(shop, plan = %{type: "application_charge"}) do redirect_uri = Application.get_env(:shopifex, :payment_redirect_uri) body = Jason.encode!(%{ application_charge: %{ name: plan.name, price: plan.price, test: test_charge?(shop, plan), return_url: "#{redirect_uri}?plan_id=#{plan.id}&shop=#{Shopifex.Shops.get_url(shop)}" } }) case HTTPoison.post( "https://#{Shopifex.Shops.get_url(shop)}/admin/api/2024-01/application_charges.json", body, "X-Shopify-Access-Token": shop.access_token, "Content-Type": "application/json" ) do {:ok, resp} -> {:ok, Jason.decode!(resp.body)["application_charge"]} end end def complete_payment(conn, %{ "charge_id" => charge_id, "plan_id" => plan_id, "shop" => shop_url }) do redirect_after_agent = Application.get_env(:shopifex, :redirect_after_agent, Shopifex.RedirectAfterAgent) # Shopify's API doesn't provide an HMAC validation on # this return-url. Use the token param in the redirect_after # url that is associated with this charge_id to validate # the request and get the current shop with redirect_after when redirect_after != nil <- redirect_after_agent.get(charge_id), redirect_after <- URI.decode_www_form(redirect_after), shop when not is_nil(shop) <- Shopifex.Shops.get_shop_by_url(shop_url) do payment_guard = Application.fetch_env!(:shopifex, :payment_guard) plan = payment_guard.get_plan(plan_id) {:ok, grant} = payment_guard.create_grant(shop, plan, charge_id) after_payment(conn, shop, plan, grant, redirect_after) else _ -> {:error, :forbidden} end end @impl ShopifexWeb.PaymentController def render_plans(conn, guard_identifier, redirect_after) do conn |> put_view(ShopifexWeb.PaymentView) |> put_layout({ShopifexWeb.LayoutView, "app.html"}) |> render("show-plans.html", guard: guard_identifier, redirect_after: redirect_after ) end @impl ShopifexWeb.PaymentController def after_payment(conn, shop, _plan, _grant, redirect_after) do api_key = Application.get_env(:shopifex, :api_key) redirect(conn, external: "https://#{Shopifex.Shops.get_url(shop)}/admin/apps/#{api_key}#{redirect_after}" ) end defoverridable render_plans: 3, after_payment: 5, test_charge?: 2 end end end