Packages
shopifex
0.5.5
2.4.0
2.3.0
2.2.2
2.2.1
2.2.0
2.1.20
2.1.19
2.1.18
2.1.17
2.1.16
2.1.15
2.1.14
2.1.13
2.1.12
2.1.11
2.1.10
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.1
2.0.0
1.1.1
1.1.0
1.0.1
1.0.0
0.6.2
0.6.1
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Phoenix boilerplate for Shopify Embedded App SDK
Current section
Files
Jump to
Current section
Files
lib/shopifex_web/controllers/payment_controller.ex
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 """
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()
@optional_callbacks after_payment: 5
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)
payment_guard.show_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 = conn.private.shop
{:ok, charge} = create_charge(shop, plan)
redirect_after_agent.set(charge["id"], redirect_after)
send_resp(conn, 200, Jason.encode!(charge))
end
# 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!) {
appSubscriptionCreate(name: $name, returnUrl: $return_url, test: $test, lineItems: [{plan: {appRecurringPricingDetails: {price: {amount: $price, currencyCode: USD}, interval: ANNUAL}}}]) {
appSubscription {
id
}
confirmationUrl
userErrors {
field
message
}
}
}
""",
%{
name: plan.name,
price: plan.price,
test: plan.test,
return_url: "#{redirect_uri}?plan_id=#{plan.id}"
},
url: "https://#{shop.url}/admin/api/2021-04/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: plan.test,
return_url: "#{redirect_uri}?plan_id=#{plan.id}"
}
})
case HTTPoison.post(
"https://#{shop.url}/admin/api/2021-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: plan.test,
return_url: "#{redirect_uri}?plan_id=#{plan.id}"
}
})
case HTTPoison.post(
"https://#{shop.url}/admin/api/2021-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
}) 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),
%URI{query: query} <- URI.parse(redirect_after),
%{"token" => token} <- URI.decode_query(query),
{:ok, shop, _claims} <- Shopifex.Guardian.resource_from_token(token) 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 after_payment(conn, shop, _plan, _grant, redirect_after) do
api_key = Application.get_env(:shopifex, :api_key)
redirect(conn, external: "https://#{shop.url}/admin/apps/#{api_key}#{redirect_after}")
end
defoverridable after_payment: 5
end
end
end