{"id":21825859,"url":"https://github.com/ericdude4/shopifex","last_synced_at":"2025-04-05T13:05:01.304Z","repository":{"id":43900760,"uuid":"266938204","full_name":"ericdude4/shopifex","owner":"ericdude4","description":"🛍️  Build Shopify Apps with Elixir Phoenix","archived":false,"fork":false,"pushed_at":"2024-06-22T16:48:46.000Z","size":4025,"stargazers_count":99,"open_issues_count":3,"forks_count":32,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-08-29T10:34:36.457Z","etag":null,"topics":["elixir","shopify"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/shopifex/readme.html#content","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ericdude4.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-26T03:44:41.000Z","updated_at":"2024-08-25T13:24:42.000Z","dependencies_parsed_at":"2024-01-11T18:03:00.363Z","dependency_job_id":"6c782bc8-a30f-43ee-997d-2983d6d62950","html_url":"https://github.com/ericdude4/shopifex","commit_stats":{"total_commits":185,"total_committers":10,"mean_commits":18.5,"dds":"0.10810810810810811","last_synced_commit":"ced666a7e3261a6a0d35ff945e6ffec8e6aabf5b"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericdude4%2Fshopifex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericdude4%2Fshopifex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericdude4%2Fshopifex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericdude4%2Fshopifex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ericdude4","download_url":"https://codeload.github.com/ericdude4/shopifex/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247339154,"owners_count":20923014,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","shopify"],"created_at":"2024-11-27T18:02:52.863Z","updated_at":"2025-04-05T13:05:01.265Z","avatar_url":"https://github.com/ericdude4.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg width=\"350\" src=\"https://github.com/ericdude4/shopifex/raw/master/guides/images/logo.png\" alt=\"Shopifex\"\u003e\n\n---\n\n[![Hex.pm](https://img.shields.io/hexpm/v/shopifex.svg)](https://hex.pm/packages/shopifex)\n\nA simple boilerplate package for creating Shopify embedded apps with the Elixir Phoenix framework. [https://hexdocs.pm/shopifex](https://hexdocs.pm/shopifex)\n\n## Installation\n\nThe package can be installed\nby adding `shopifex` to your list of dependencies in `mix.exs`: (note, OTP 22 or greater required)\n\n```elixir\ndef deps do\n  [\n    {:shopifex, \"~\u003e 2.2\"}\n  ]\nend\n```\n## Quickstart\n#### Run the install script\nThis will install all of the supported Shopifex features.\n```\nmix shopifex.install\n```\nFollow the output `config.ex` and `router.ex` instructions from the install script.\n#### Run migrations\n```\nmix ecto.migrate\n```\n#### Update Shopify app details\nReplace tunnel-url with your own where applicable.\n- Set \"App URL\" to `https://my-app.ngrok.io/auth`\n- Add `https://my-app.ngrok.io/auth/install` \u0026 `https://my-app.ngrok.io/auth/update` to your app's \"Allowed redirection URL(s)\"\n- Add your Shopify app's API key and API secret key to `config :shopifex, api_key: \"your-api-key\", secret: \"your-api-secret\"`\n\n## Manual Installation\nCreate the shop schema where the installation data will be stored:\n```\nmix phx.gen.schema Shop shops url:string access_token:string scope:string\nmix ecto.migrate\n```\n\nAdd the `:shopifex` config settings to your `config.ex`. More config details [here](https://hexdocs.pm/shopifex)\n\n```elixir\nconfig :shopifex,\n  app_name: \"MyApp\",\n  shop_schema: MyApp.Shop,\n  web_module: MyAppWeb,\n  repo: MyApp.Repo,\n  redirect_uri: \"https://myapp.ngrok.io/auth/install\",\n  reinstall_uri: \"https://myapp.ngrok.io/auth/update\",\n  webhook_uri: \"https://myapp.ngrok.io/webhook\",\n  scopes: \"read_inventory,write_inventory,read_products,write_products,read_orders\",\n  api_key: \"shopifyapikey123\",\n  secret: \"shopifyapisecret456\",\n  webhook_topics: [\"app/uninstalled\"], # These are automatically subscribed on a store upon install\n  allowed_drift: 10_000 # session token exp/nbf tolerance in ms (defaults to 10s)\n```\n\nUpdate your `endpoint.ex` to include the custom body parser. This is necessary for HMAC validation to work.\n\n```elixir\nplug Plug.Parsers,\n  parsers: [:urlencoded, :multipart, :json],\n  pass: [\"*/*\"],\n  body_reader: {ShopifexWeb.CacheBodyReader, :read_body, []},\n  json_decoder: Phoenix.json_library()\n```\n\nAdd this line near the top of `router.ex` to include the Shopifex pipelines\n\n```elixir\nrequire ShopifexWeb.Routes\nShopifexWeb.Routes.pipelines()\n```\nNow the following pipelines are accessible:\n\n- `:shopify_session` -\u003e Validates request (HMAC header/param or token param) and makes session information available via `Shopifex.Plug` API. Also removes iFrame blocking headers so app can render in Shopify admin.\n- `:shopify_webhook` -\u003e Validates Shopify webhook requests HMAC and makes session information available via `Shopifex.Plug` API.\n- `:shopify_admin_link` -\u003e Validates Shopify admin link \u0026 bulk action link requests and makes session information available via `Shopifex.Plug` API.\n- `:shopify_api` -\u003e Ensures that a valid Shopify session token or Shopifex token are present in `Authorization` header. Useful for async requests between your SPA front end and Shopifex backend.\n- `:shopifex_browser` -\u003e Same as your normal `:browser` pipeline, except it calls `Shopifex.Plug.LoadInIframe`.  Deprecated; does not work with Phoenix 1.6 generated apps.\n\nNow add this basic example of these plugs in action in `router.ex`. These endpoints need to be added to your Shopify app whitelist\n\n### Routing\n```elixir\n# Include all auth (when Shopify requests to render your app in an iframe), installation and update routes \nShopifexWeb.Routes.auth_routes(MyAppWeb.AuthController)\n\n# Add the LoadInIframe plug to your existing :browser pipeline\npipeline :browser do\n  # ... Other plugs\n  plug Shopifex.Plug.LoadInIframe\nend\n\n# Endpoints accessible within the Shopify admin panel iFrame.\n# Don't include this scope block if you are creating a SPA.\nscope \"/\", MyAppWeb do\n  pipe_through [:browser, :shopify_session]\n\n  get \"/\", PageController, :index\nend\n\n# Make your webhook endpoint look like this\nscope \"/webhook\", MyAppWeb do\n  pipe_through [:shopify_webhook]\n\n  post \"/\", WebhookController, :action\nend\n\n# Place your admin link endpoints in here\nscope \"/admin-links\", MyAppWeb do\n  pipe_through [:shopify_admin_link]\n\n  get \"/do-a-thing\", AdminLinkController, :do_a_thing\nend\n```\n\nCreate a new controller called `auth_controller.ex` to handle the initial iFrame load and installation\n\n```elixir\ndefmodule MyAppWeb.AuthController do\n  use MyAppWeb, :controller\n  use ShopifexWeb.AuthController\n\n  # Thats it! Validation, installation are now handled for you :)\n  \n  # Optionally, override the `after_install` callback\n  def after_install(conn, shop, oauth_state) do\n    # TODO: send yourself an e-mail\n    # follow default behaviour.\n    super(conn, shop, oauth_state)\n  end\nend\n```\nSetting up your application as a SPA? Read this before continuing [Single Page Applications](#single-page-applications)\n\ncreate another controller called `webhook_controller.ex` to handle incoming Shopify webhooks (optional)\n\n```elixir\ndefmodule MyAppWeb.WebhookController do\n  use MyAppWeb, :controller\n  use ShopifexWeb.WebhookController\n\n  # add as many handle_topic/3 functions here as you like! This basic one handles app uninstallation\n  def handle_topic(conn, shop, \"app/uninstalled\") do\n    Shopifex.Shops.delete_shop(shop)\n\n    conn\n    |\u003e send_resp(200, \"success\")\n  end\n\n  # Mandatory Shopify shop data erasure GDPR webhook. Simply delete the shop record\n  def handle_topic(conn, shop, \"shop/redact\") do\n    Shopifex.Shops.delete_shop(shop)\n\n    conn\n    |\u003e send_resp(204, \"\")\n  end\n\n  # Mandatory Shopify customer data erasure GDPR webhook. Simply delete the shop (customer) record\n  def handle_topic(conn, shop, \"customers/redact\") do\n    # If you store customer data you can delete it here.\n\n    conn\n    |\u003e send_resp(204, \"\")\n  end\n\n  # Mandatory Shopify customer data request GDPR webhook.\n  def handle_topic(conn, _shop, \"customers/data_request\") do\n    # Send an email of the shop data to the customer.\n    conn\n    |\u003e send_resp(202, \"Accepted\")\n  end\nend\n```\n## Maintaining session between page loads for server-rendered applications\nAs browsers continue to restrict cookies, cookies become more unreliable as a method for maintaining a session within an iFrame. To address this, Shopify recommends passing a JWT session token back and forth between requests.\n\nShopifex makes a token accessible with `Shopifex.Plug.session_token(conn)` in any request which passes through a `:shopify_*` router pipeline.\n\nEnsure there is a `token` parameter sent along in any requests which you would like to maintain session between.\n\nEEx template link:\n```elixir\n\u003c%= link \"home\", to: Routes.page_path(@conn, :index, %{token: Shopifex.Plug.session_token(conn)}) %\u003e\n```\nEEx template form:\n```elixir\n\u003c%= form_for :foo, Routes.foo_path(MyApp.Endpoint, :new, %{token: Shopifex.Plug.session_token(@conn)}), fn f -\u003e %\u003e\n  \u003c%= submit \"Submit\" %\u003e\n\u003c% end %\u003e\n```\n\n## Using LiveView in your embedded app\nThere are two special considerations to using LiveView in your embedded app.\n\nFirst, you'll need to get the LiveView socket configured to work in the Shopify iframe. This [elixir](https://elixirforum.com/t/how-to-embed-a-liveview-via-iframe/65066) post gives some excellent tips.\n\nSecond, you'll need to copy the `current_shop` and `session_token` from the Plug connection to the socket and make them available in your assigns on_mount. The `@current_shop` will be your authenticated Shop resource, and `@session_token` can be used when you navigate between live views similar to the template links above.  The `shopifex_live_session` macro is a drop-in replacement fom `live_session` to handle this.\n\n```\nscope \"/\", ShoplensWeb do\n  pipe_through [:shopifex_browser, :shopify_session]\n\n  ShopifexWeb.Routes.shopifex_live_session :embedded, layout: {MyApp.Layouts, :embedded} do\n    live \"/\", MyAppLive\n    ...\n  end\n\n  # If you need more control, you can still use `live_session` like this:\n  #  live_session :embedded, \n  #    session: {ShopifexWeb.LiveSession, :put_shop_in_session, []}, \n  #    on_mount: {ShopifexWeb.LiveSession, :assign_shop_to_socket} do\n  #       ...\n  #   end\nend\n```\n\n## Update app permissions\n\nYou can also update the app permissions after installation. To do so, first you have to add `your-redirect-url.com/auth/update` to Shopify's whitelist.\n\nTo add e.g. the `read_customers` scope, you can do so by redirecting them to the following example url:\n\n```\nhttps://{shop-name}.myshopify.com/admin/oauth/request_grant?client_id=API_KEY\u0026redirect_uri={YOUR_REINSTALL_URL}/auth/update\u0026scope={YOUR_SCOPES},read_customers\n```\n\n## Add payment guards to routes\nThis system allows you to use the `Shopifex.Plug.PaymentGuard` plug. If the merchant does not have an active grant associated with the named guard, it will redirect them to a plan selection page, allow them to pay, and handle the payment callback all automatically. I am working on the admin panel where you can register Plan objects which grant `premium_plan` (for example) - but for now these need to be entered manually into the database.\n\nGenerate the schemas\n\n`mix phx.gen.schema Shops.Plan plans name:string price:string features:array:string grants:array:string test:boolean usages:integer type:string`\n\n`mix phx.gen.schema Shops.Grant grants shop_id:references:shops charge_id:integer grants:array:string remaining_usages:integer total_usages:integer`\n\nAdd the config options:\n```elixir\nconfig :my_app,\n  payment_guard: MyApp.Shops.PaymentGuard,\n  grant_schema: MyApp.Shops.Grant,\n  plan_schema: MyApp.Shops.Plan,\n  payment_redirect_uri: \"https://myapp.ngrok.io/payment/complete\"\n```\nServe the Shopifex assets for the plans selection page. Add the following to `endpoint.ex`:\n```elixir\n# Serve at \"/shopifex-assets\" the static files from shopifex.\nplug Plug.Static,\n  at: \"/shopifex-assets\",\n  from: :shopifex,\n  gzip: false,\n  only: ~w(css fonts images js favicon.ico robots.txt)\n```\nCreate the payment guard module:\n```elixir\ndefmodule MyApp.Shops.PaymentGuard do\n  use Shopifex.PaymentGuard\nend\n```\nCreate a new payment controller:\n```elixir\ndefmodule MyAppWeb.PaymentController do\n  use MyAppWeb, :controller\n  use ShopifexWeb.PaymentController\nend\n```\nAdd payment routes to `router.ex`:\n```elixir\nShopifexWeb.Routes.payment_routes(MyAppWeb.PaymentController)\n```\n\nTo manage plans, I recommend using [kaffy admin package](https://github.com/aesmail/kaffy)\n\nNow you can protect routes or controller actions with the `Shopifex.Plug.PaymentGuard` plug. Here is an example of it in action on an admin link\n```elixir\ndefmodule MyAppWeb.AdminLinkController do\n  use MyAppWeb, :controller\n  require Logger\n\n  plug Shopifex.Plug.PaymentGuard, \"premium_plan\" when action in [:premium_function]\n  \n  def premium_function(conn, _params) do\n    shop = Shopifex.Plug.current_shop(conn)\n    \n    # Wow, much premium.\n    conn\n    |\u003e send_resp(200, \"Hi there, #{shop.url}!\")\n  end\nend\n```\n### Single Page Applications\nSPA Shopify applications are also supported with Shopifex for developers who wish to host their front-end application separately from the back-end. This approach takes advantage of [Shopify session tokens](https://shopify.dev/concepts/apps/building-embedded-apps-using-session-tokens).\n\nAdjust your `router.ex` file. You may notice some routes are no longer necessary compared to the quick-start guide.\n```elixir\nShopifexWeb.Routes.pipelines()\n\n# These routes will take care of installation/update\nShopifexWeb.Routes.auth_routes(ShopifyAppWeb)\n\n# API routes for your SPA to hit with the axios instance\nscope \"/api\", MyAppWeb do\n  pipe_through [:shopify_api]\n  \n  # An endpoint which your SPA can call on load to get whatever initialization data your app needs.\n  # The options macro is required to allow CORS requests on the route.\n  options \"/initialize\", AuthController, :initialize\n  get \"/initialize\", AuthController, :initialize\n  \n  # Add authenticated routes here as needed.\nend\n```\nAnd for that `/initialize` endpoint, consider this adjustment to `MyAppWeb.AuthController` and update based on your needs. Perhaps you also want to serialize and return some more information needed by your SPA at startup.\n```elixir\ndefmodule MyAppWeb.AuthController do\n  use MyAppWeb, :controller\n  use ShopifexWeb.AuthController\n  \n  def initialize(conn, _params) do\n    shop = Guardian.Plug.current_resource(conn)\n    \n    render(conn, \"initialize.json\", %{shop: shop})\n  end\nend\n```\nNow, [integrate Shopify session tokens into the Axios instance of your SPA.](https://shopify.dev/tutorials/use-session-tokens-with-axios)\nThen from your SPA:\n```javascript\nimport createApp from '@shopify/app-bridge';\n// Import your Shopify session_token axios instance based on the Shopify session token axios instructions\nimport instance from './axios-instance';\n\nconst urlParams = new URLSearchParams(window.location.search);\nconst shopOrigin = urlParams.get('shop');\n\nwindow.app = createApp({\n  apiKey: \"MY_SHOPIFY_API_KEY\",\n  shopOrigin,\n});\n\n// Use your axios instance to call the /api/initialize endpoint\nconst sessionData = await instance.get('/api/initialize');\n// Now you will have access to the current shop and Bob's-yer-uncle!\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericdude4%2Fshopifex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fericdude4%2Fshopifex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericdude4%2Fshopifex/lists"}