From bfcc1be0a88a852e74bf27aa4f93329837148060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 21 Dec 2022 18:06:35 +0000 Subject: [PATCH 01/35] feat: Adding payment processing concepts section. #1 --- README.md | 330 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 328 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 13d33d9..5a7dee0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,328 @@ -# learn-payment-processing -[WiP] Learn how to process online payments in your web application! :money: +
+ +# Learn Payment Processing + +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/learn-payment-processing/ci.yml?label=build&style=flat-square&branch=main) +[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/learn-payment-processing/main.svg?style=flat-square)](https://codecov.io/github/dwyl/phoenix-chat-example?branch=main) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/phoenix-chat-example/issues) +[![HitCount](https://hits.dwyl.com/dwyl/learn-payment-processing.svg?style=flat-square&show=unique)](http://hits.dwyl.com/dwyl/learn-payment-processing) + +Learn what payment processing is +and how you can add it to your application! + +
+ +# Why? 🤷 + +Not all applications are free. +There are some that can be acquired through +a [one time purchase](https://git-fork.com/buy). +There are others +that are [subscription-based](https://www.notion.so/pricing). +Regardless of the type, all of these share one thing in common: +**they use payment processing platforms/gateways +to manage transactions**. + +In any current application, +knowing how processing payments *work* +and how they *can* be implemented +is important, +as it affects the bottom-line +of the project/application/company. + +# What 💭 + +Let's think for a minute of what happens +when we purchase something online. +Assume we want to [buy Fork](https://git-fork.com/buy), +a neat open-source Git GUI. + +buy-fork-1 + +If we click the `Buy Fork` button, +we are met with a modal +for a one-time purchase license. + +buy-fork-modal + +If we proceed to payment, +we can pay through Paypal +or Apple Pay +or even with any credit-card! + +buy-fork-options + +But how was this implemented? +Is there any service behind this? +For those with a keen eye, +you might have noticed +that in the lower third of the screen, +it seems that this transaction +is being handled by +[`Paddle.com`](https://www.paddle.com/). + +`Paddle.com` can be described as a +*payment infrastructure provider* +that takes care of transactions +and payments made by the users of your application +and facilitates integrating payments to your application. +By using these types of services, +it is much easier for *us* developers +to provide different ways of users to pay +and integrate payments *seamlessly* in our applications. + +But, as you may be aware, +there are several other SaaS providers, +such as [`Stripe`](https://stripe.com/en-pt) +or [`Square`](https://squareup.com/us/en/). +Each one of these may differ from each others. + +But we are getting ahead of ourselves. +We need to clarify some concepts before moving forward. + +Ready to start? 🏁 + +## How payment works + +Let's start with an analogy. +Imagine you want to send a parcel to your father. +1. You first drop it in a DHL (or UPS) drop-off point. +2. DHL collects the package and transports the parcel to your father. +3. Once the delivery is complete, +a confirmation e-mail is sent to you and your father. + +Think of the DHL distribution network +as analogous to a **payment processor**. +And think of the drop-off point +as a **payment gateway**. +To make an online payment, +customers create a transaction +*via payment gateway* +and the *payment processor facilitates communication +between parties* and transfers funds +into the merchant's bank account. + +gateway_v_processor + +So, a **payment processor** functions as an intermediary +between the customer's party +(which consists of the user and his bank) +and the merchant's +(which consists of the merchant and his bank). +It is the entity responsible for communicating +between both parties in the transaction. + +Meanwhile, the **payment gateway** +is a *point of sale* for online payments. +Similarly to when a customer swipes his card +on a physical credit card terminal, +online stores need a gateway to securely collect +the customer's payment information. +So *payment gateway is basically a virtual terminal, +and functions as a point of sale*. + +This whole process of online payment +usually assumes the merchant +has a [**merchant account**](https://www.investopedia.com/terms/m/merchant-account.asp). +A merchant account is simply +a type of business bank account +that *allows a business +to receive credit card +and electronic transactions*. + +## Okay... but where does `Paddle` come into all of this? + +Now that we got the important concepts out of the way, +you might be wondering: +"That's cool, but what does this have to do +with the `Paddle` example you gave earlier?" + +Glad you asked! 👍 + +If you've done online shopping before, +you probably came across a button like this, +which allowed you to purchase the item through Paypal. + +![paypal](https://user-images.githubusercontent.com/17494745/208951049-421e123a-e082-433e-8b08-60c7da8c8a57.png) + +If you wanted to add a way for users to purchase +an item in your application through Paypal, +you'd have to setup a Paypal account +and use [one of their SDKs](https://developer.paypal.com/home) +to make it possible for customers to buy +through Paypal. + +You are basically using the [Paypal E-commerce platform](https://www.paypal.com/us/business/platforms-and-marketplaces) +to setup a payment gateway and processor +for users to pay with Paypal on your site. + +Awesome! 🎉 + +Let's say you now want to add +[`Google Pay`](https://pay.google.com/about/business/implementation/), +as a payment method, as well. +You'd have to create an account, +use [their SDK](https://developers.google.com/pay/api) +and integrate it in your website. + +We shouldn't forget iPhone users as well! +We also want (need!) to add an [`Apple Pay`](https://www.apple.com/apple-pay/) +payment method to our application. +Same procedure occurs, we ought to create an account +and use [their SDK](https://developer.apple.com/apple-pay/implementation/) +to integrate it into our application. + +On top of this, we need to create our own merchant account +so each one of these services can connect to it +and process the transactions. + +This is a heap of effort +and can easily scale to unsustainable levels. +This is where **payment platforms** +like Stripe come into play. + +## Payment platforms + +Payment platforms **simplify the process of connecting +to multiple third-parties**. +It offers more than a payment service +so that merchants only have to liaise with one company +rather than multiple ones. + +This has a great impact on how an application +is *designed* and *implemented*, +and allows to for a better management +and [decoupling](https://en.wikipedia.org/wiki/Single-responsibility_principle) +of responsibilities. + +Instead of our own API having to manage different providers, +we can use a platform like `Stripe` to do the work for us. +This is how the application should be laid out! + +![design](https://user-images.githubusercontent.com/17494745/208956397-cda6d895-8034-45b0-bc91-61befb012fb3.png) + +As you can see, it is much simpler! +By offering a bundle of essential payment technologies, +these companies are reducing the merchant's work +of having to manage each of them separately. +In addition to this, there are a number of other advantages, +such as security, data monitoring and reporting. + +For example, `Stripe` is like having a multiple +**payment processors**, **payment gateways** and **merchant account** +bundled into one, +along with a [myriad of other features](https://stripe.com/en-pt). + +## Stripe and alternatives + +`Stripe` is considered by many to be +the [*de facto*](https://trends.builtwith.com/payment/Stripe) +way of accepting credit cards +and electronic payments on the web. +It's a powerful payment tool +that has a number of additional features, +including [smart retries](https://stripe.com/docs/billing/revenue-recovery/smart-retries), +[automatic card updater](https://stripe.com/docs/saving-cards), +[fraud tooling](https://stripe.com/en-gb-pt/radar), +and [others](https://stripe.com/partners/directory). + +However, it is important to note that there are +several other options that do offer similar features, +ease of payment integration into your application +but handle payments in a different way. + +For example, `Paddle`, as we have mentioned earlier, +works on a completely different way. +While `Stripe` can be compared to a payment gateway +that deals with multiple channels, +`Paddle` offers similar features +but acts a *reseller of your services* - +**merchant of record (MoR)**. + +A MoR is ["a term to describe the legal entity +selling goods or services to an end customer"](https://www.paddle.com/blog/what-is-merchant-of-record). +It's who the end customer owes payment for their purchase, +and it is who handles payments and liability for each transaction. +This is great for *tax handling*, +[which is especially relevant in Europe](https://www.outseta.com/posts/startup-payment-processing) +and one of the reasons people +[consider `Paddle` in lieu of `Stripe`](https://splitbee.io/blog/why-we-moved-from-stripe-to-paddle). +`Stripe` is making strides in also having +[better tax compliance](https://stripe.com/newsroom/news/taxjar) +but it's not quite there, at the moment of writing. + +![mor](https://images.prismic.io/paddle/d6ff57bb-31dd-41ed-86c6-23d73584b617_merchant+of+record.png?auto=format&fit=crop&ixlib=react-9.5.4) + +So businesses can choose to be their own merchant of record +and setup an infrastructure and processes needed to manage +payments with `Stripe` and deal with liabilities +and tax handling themselves. +*Or* they can use MoR service providers like `Paddle` +who take the burden of all of payment processing +and legal compliance away. Of course, these usually incur higher fees than `Stripe`. + +## I'm confused. Which one should I choose? + +[That's a great question](https://www.indiehackers.com/post/stripe-vs-paddle-89161b0d5c). + +[Which seems to asked](https://splitbee.io/blog/why-we-moved-from-stripe-to-paddle) +[over](https://stackshare.io/stackups/paddle-vs-stripe), +and [over](https://www.paddle.com/compare/stripe) +and [over](https://www.reddit.com/r/SaaS/comments/q3kao9/paddle_vs_chargebee_vs_stripe_any_recommendations/) again. + +Depending on the use-case or your choice, +each product provides different pricing plans, fees and features +and you should make this decision +based on the requirements of your project +and how much you are willing to spend. + +What's important here is you know +*how online payments work*, +what parties are involved +and how you can **leverage these platforms** +to make this process easier. + +Remember, we are dealing with **sensitive information**. +Credit card info should be handled with *extreme caution* +amd these platforms makes it easier for us to do just that. + +But implementation-wise, +when designing and implementing your application, +you should notice that +the process is similar between whatever alternative you choose. +Your application will make use of their SDKs +to integrate different channels +and payment alternatives to process transactions. + +And guess what, +you are going to be doing that in the next section! + +Race you there! 🏃 + +# How? 💻 + +In this section, +we are going to be implementing an example application +using Stripe with Elixir. + +> If you have never programmed in Elixir, +we recommend checking the +[`learn-elixir`](https://github.com/dwyl/learn-elixir) +and [`learn-phoenix-framework`](https://github.com/dwyl/learn-phoenix-framework) +repositories before you start this walk-through, +as we are going to be focusing on payment processing, +and not on the basics of a Phoenix project. + +The reason we went with `Stripe` +and not any other alternative like `Paddle` +is because `Stripe` allows us to create an account +without having to fill business-related information, +KYC and a company website. + +# Thanks! + +Thanks for learning about payment processing with us! +If you have any questions, please ask!! +Please ⭐ this repo to help spread the word! + +If you are using environment variables in a way not mentioned in this readme, or have a better way of managing them or any other ideas or suggestions for improvements please tell us!! \ No newline at end of file From b67f40668f04d6a99b969ef448fddbd9f064fdaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 21 Dec 2022 19:09:14 +0000 Subject: [PATCH 02/35] feat: Adding steps for Stripe account creation. #1 --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 5a7dee0..1434347 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,15 @@ that *allows a business to receive credit card and electronic transactions*. +Do note that the terms `"payment processor"` and `"payment gateway"` +usually fall under the same term - `payment processor`. +This is because they work together in achieving +the payment processing. +So if you see platforms like `Stripe` +being mentioned as a "payment processor", +it's because it offers both `payment gateway` and `payment processor` +bundled together (alongside a myriad of other features). + ## Okay... but where does `Paddle` come into all of this? Now that we got the important concepts out of the way, @@ -319,6 +328,32 @@ is because `Stripe` allows us to create an account without having to fill business-related information, KYC and a company website. +# Pre-requisites + +For this tutorial, +you will need to create a `Stripe` account. +If you visit https://dashboard.stripe.com/register, +you will be prompted to create an account. + +stripe-create + +After filling your information, +and verifying your account, +you will be able to access the main dashboard. +If you type "API" inside the search box +and choose `Developers > API Keys`... + +dashboard + +You will be prompted with the following window. + +Screenshot 2022-12-21 at 19 02 28 + +These API keys will later be used in the tutorial. +Save them and don't show them to anyone else! +We are going to be using these +as [environment variables](https://github.com/dwyl/learn-environment-variables). + # Thanks! Thanks for learning about payment processing with us! From 52e8e50ea1dda267ef96ff6f8bd68941c1e1fce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Thu, 22 Dec 2022 10:15:16 +0000 Subject: [PATCH 03/35] fix: Switching Notion to Crunchyroll. #1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1434347..58e94da 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Not all applications are free. There are some that can be acquired through a [one time purchase](https://git-fork.com/buy). There are others -that are [subscription-based](https://www.notion.so/pricing). +that are [subscription-based](https://www.crunchyroll.com/welcome). Regardless of the type, all of these share one thing in common: **they use payment processing platforms/gateways to manage transactions**. From f6b02c4ec866cc8199cd03428c853f0149d7cd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Thu, 22 Dec 2022 12:55:58 +0000 Subject: [PATCH 04/35] feat: Initial project commit. #1 --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 35 + README.md | 12 +- assets/css/app.css | 5 + assets/js/app.js | 41 ++ assets/tailwind.config.js | 26 + assets/vendor/topbar.js | 167 +++++ config/config.exs | 64 ++ config/dev.exs | 80 +++ config/prod.exs | 21 + config/runtime.exs | 115 ++++ config/test.exs | 33 + lib/app.ex | 9 + lib/app/application.ex | 38 ++ lib/app/mailer.ex | 3 + lib/app/repo.ex | 5 + lib/app_web.ex | 114 ++++ lib/app_web/components/core_components.ex | 623 ++++++++++++++++++ lib/app_web/components/layouts.ex | 5 + lib/app_web/components/layouts/app.html.heex | 55 ++ lib/app_web/components/layouts/root.html.heex | 17 + lib/app_web/controllers/error_html.ex | 19 + lib/app_web/controllers/error_json.ex | 15 + lib/app_web/controllers/page_controller.ex | 9 + lib/app_web/controllers/page_html.ex | 5 + .../controllers/page_html/home.html.heex | 236 +++++++ lib/app_web/endpoint.ex | 51 ++ lib/app_web/gettext.ex | 24 + lib/app_web/router.ex | 44 ++ lib/app_web/telemetry.ex | 92 +++ mix.exs | 72 ++ mix.lock | 43 ++ priv/gettext/en/LC_MESSAGES/errors.po | 112 ++++ priv/gettext/errors.pot | 110 ++++ priv/repo/migrations/.formatter.exs | 4 + priv/repo/seeds.exs | 11 + priv/static/favicon.ico | Bin 0 -> 1258 bytes priv/static/images/phoenix.png | Bin 0 -> 13900 bytes priv/static/robots.txt | 5 + test/app_web/controllers/error_html_test.exs | 14 + test/app_web/controllers/error_json_test.exs | 12 + .../controllers/page_controller_test.exs | 8 + test/support/conn_case.ex | 38 ++ test/support/data_case.ex | 58 ++ test/test_helper.exs | 2 + 45 files changed, 2451 insertions(+), 1 deletion(-) create mode 100644 .DS_Store create mode 100644 assets/css/app.css create mode 100644 assets/js/app.js create mode 100644 assets/tailwind.config.js create mode 100644 assets/vendor/topbar.js create mode 100644 config/config.exs create mode 100644 config/dev.exs create mode 100644 config/prod.exs create mode 100644 config/runtime.exs create mode 100644 config/test.exs create mode 100644 lib/app.ex create mode 100644 lib/app/application.ex create mode 100644 lib/app/mailer.ex create mode 100644 lib/app/repo.ex create mode 100644 lib/app_web.ex create mode 100644 lib/app_web/components/core_components.ex create mode 100644 lib/app_web/components/layouts.ex create mode 100644 lib/app_web/components/layouts/app.html.heex create mode 100644 lib/app_web/components/layouts/root.html.heex create mode 100644 lib/app_web/controllers/error_html.ex create mode 100644 lib/app_web/controllers/error_json.ex create mode 100644 lib/app_web/controllers/page_controller.ex create mode 100644 lib/app_web/controllers/page_html.ex create mode 100644 lib/app_web/controllers/page_html/home.html.heex create mode 100644 lib/app_web/endpoint.ex create mode 100644 lib/app_web/gettext.ex create mode 100644 lib/app_web/router.ex create mode 100644 lib/app_web/telemetry.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 priv/gettext/en/LC_MESSAGES/errors.po create mode 100644 priv/gettext/errors.pot create mode 100644 priv/repo/migrations/.formatter.exs create mode 100644 priv/repo/seeds.exs create mode 100644 priv/static/favicon.ico create mode 100644 priv/static/images/phoenix.png create mode 100644 priv/static/robots.txt create mode 100644 test/app_web/controllers/error_html_test.exs create mode 100644 test/app_web/controllers/error_json_test.exs create mode 100644 test/app_web/controllers/page_controller_test.exs create mode 100644 test/support/conn_case.ex create mode 100644 test/support/data_case.ex create mode 100644 test/test_helper.exs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7b5b60656771183acb716b87dd66bc09cfb608ee GIT binary patch literal 6148 zcmeHKK~BRk5FEErN#W8X7o@zP5nX9sr~*RYjnPwy4M4`2%0zeVky{wkpRd zCnSW>Ze>s6%uY6wCXNA^(PnxI3;?8Tf}K9QJt4=XwW8xk4vEHURG6Mu%Sl-+%NAP) z{-OhN?|QgKi47KL-aq~+%n6SVKW*6Zd$`4lzZuq81@~IGuPyEbOFZI^%JYc(5pfet zsreLZcIFYU%NiFbn`$^C>n*7A01sqsF{I`;*2H_nPIv5z^-gf#RJR_4>Iv(7rv6LB zlj{$8eOzM9+6r6{hdfL8bjTwom4xmlbah5|*O9Wg6)JOALmf~D)PWy3z&TqYJ#whM zI-m}y13L%g{gAN<<{oQ@_SM18E&+%Mhi!0MK5Qf>^O$?A9r6gnxKyG`HGYX topbar.delayedShow(200)) +window.addEventListener("phx:page-loading-stop", info => topbar.hide()) + +// connect if there are any LiveViews on the page +liveSocket.connect() + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket + diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js new file mode 100644 index 0000000..b611701 --- /dev/null +++ b/assets/tailwind.config.js @@ -0,0 +1,26 @@ +// See the Tailwind configuration guide for advanced usage +// https://tailwindcss.com/docs/configuration + +const plugin = require("tailwindcss/plugin") + +module.exports = { + content: [ + "./js/**/*.js", + "../lib/*_web.ex", + "../lib/*_web/**/*.*ex" + ], + theme: { + extend: { + colors: { + brand: "#FD4F00", + } + }, + }, + plugins: [ + require("@tailwindcss/forms"), + plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), + plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), + plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), + plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])) + ] +} \ No newline at end of file diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js new file mode 100644 index 0000000..4176ede --- /dev/null +++ b/assets/vendor/topbar.js @@ -0,0 +1,167 @@ +/** + * @license MIT + * topbar 1.0.0, 2021-01-06 + * Modifications: + * - add delayedShow(time) (2022-09-21) + * http://buunguyen.github.io/topbar + * Copyright (c) 2021 Buu Nguyen + */ +(function (window, document) { + "use strict"; + + // https://gist.github.com/paulirish/1579671 + (function () { + var lastTime = 0; + var vendors = ["ms", "moz", "webkit", "o"]; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = + window[vendors[x] + "RequestAnimationFrame"]; + window.cancelAnimationFrame = + window[vendors[x] + "CancelAnimationFrame"] || + window[vendors[x] + "CancelRequestAnimationFrame"]; + } + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function (id) { + clearTimeout(id); + }; + })(); + + var canvas, + currentProgress, + showing, + progressTimerId = null, + fadeTimerId = null, + delayTimerId = null, + addEvent = function (elem, type, handler) { + if (elem.addEventListener) elem.addEventListener(type, handler, false); + else if (elem.attachEvent) elem.attachEvent("on" + type, handler); + else elem["on" + type] = handler; + }, + options = { + autoRun: true, + barThickness: 3, + barColors: { + 0: "rgba(26, 188, 156, .9)", + ".25": "rgba(52, 152, 219, .9)", + ".50": "rgba(241, 196, 15, .9)", + ".75": "rgba(230, 126, 34, .9)", + "1.0": "rgba(211, 84, 0, .9)", + }, + shadowBlur: 10, + shadowColor: "rgba(0, 0, 0, .6)", + className: null, + }, + repaint = function () { + canvas.width = window.innerWidth; + canvas.height = options.barThickness * 5; // need space for shadow + + var ctx = canvas.getContext("2d"); + ctx.shadowBlur = options.shadowBlur; + ctx.shadowColor = options.shadowColor; + + var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); + for (var stop in options.barColors) + lineGradient.addColorStop(stop, options.barColors[stop]); + ctx.lineWidth = options.barThickness; + ctx.beginPath(); + ctx.moveTo(0, options.barThickness / 2); + ctx.lineTo( + Math.ceil(currentProgress * canvas.width), + options.barThickness / 2 + ); + ctx.strokeStyle = lineGradient; + ctx.stroke(); + }, + createCanvas = function () { + canvas = document.createElement("canvas"); + var style = canvas.style; + style.position = "fixed"; + style.top = style.left = style.right = style.margin = style.padding = 0; + style.zIndex = 100001; + style.display = "none"; + if (options.className) canvas.classList.add(options.className); + document.body.appendChild(canvas); + addEvent(window, "resize", repaint); + }, + topbar = { + config: function (opts) { + for (var key in opts) + if (options.hasOwnProperty(key)) options[key] = opts[key]; + }, + delayedShow: function(time) { + if (showing) return; + if (delayTimerId) return; + delayTimerId = setTimeout(() => topbar.show(), time); + }, + show: function () { + if (showing) return; + showing = true; + if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); + if (!canvas) createCanvas(); + canvas.style.opacity = 1; + canvas.style.display = "block"; + topbar.progress(0); + if (options.autoRun) { + (function loop() { + progressTimerId = window.requestAnimationFrame(loop); + topbar.progress( + "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) + ); + })(); + } + }, + progress: function (to) { + if (typeof to === "undefined") return currentProgress; + if (typeof to === "string") { + to = + (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 + ? currentProgress + : 0) + parseFloat(to); + } + currentProgress = to > 1 ? 1 : to; + repaint(); + return currentProgress; + }, + hide: function () { + clearTimeout(delayTimerId); + delayTimerId = null; + if (!showing) return; + showing = false; + if (progressTimerId != null) { + window.cancelAnimationFrame(progressTimerId); + progressTimerId = null; + } + (function loop() { + if (topbar.progress("+.1") >= 1) { + canvas.style.opacity -= 0.05; + if (canvas.style.opacity <= 0.05) { + canvas.style.display = "none"; + fadeTimerId = null; + return; + } + } + fadeTimerId = window.requestAnimationFrame(loop); + })(); + }, + }; + + if (typeof module === "object" && typeof module.exports === "object") { + module.exports = topbar; + } else if (typeof define === "function" && define.amd) { + define(function () { + return topbar; + }); + } else { + this.topbar = topbar; + } +}.call(this, window, document)); diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..9babedd --- /dev/null +++ b/config/config.exs @@ -0,0 +1,64 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. + +# General application configuration +import Config + +config :app, + ecto_repos: [App.Repo] + +# Configures the endpoint +config :app, AppWeb.Endpoint, + url: [host: "localhost"], + render_errors: [ + formats: [html: AppWeb.ErrorHTML, json: AppWeb.ErrorJSON], + layout: false + ], + pubsub_server: App.PubSub, + live_view: [signing_salt: "u48tsthN"] + +# Configures the mailer +# +# By default it uses the "Local" adapter which stores the emails +# locally. You can see the emails in your browser, at "/dev/mailbox". +# +# For production it's recommended to configure a different adapter +# at the `config/runtime.exs`. +config :app, App.Mailer, adapter: Swoosh.Adapters.Local + +# Configure esbuild (the version is required) +config :esbuild, + version: "0.14.41", + default: [ + args: + ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), + cd: Path.expand("../assets", __DIR__), + env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} + ] + +# Configure tailwind (the version is required) +config :tailwind, + version: "3.1.8", + default: [ + args: ~w( + --config=tailwind.config.js + --input=css/app.css + --output=../priv/static/assets/app.css + ), + cd: Path.expand("../assets", __DIR__) + ] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..0c420cf --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,80 @@ +import Config + +# Configure your database +config :app, App.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "app_dev", + stacktrace: true, + show_sensitive_data_on_connection_error: true, + pool_size: 10 + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we use it +# with esbuild to bundle .js and .css sources. +config :app, AppWeb.Endpoint, + # Binding to loopback ipv4 address prevents access from other machines. + # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. + http: [ip: {127, 0, 0, 1}, port: 4000], + check_origin: false, + code_reloader: true, + debug_errors: true, + secret_key_base: "bv1heeBXJGvlEsc6SI66xUox+004UlT+aRAH+UlGgMxGuGMXCbEK32pVx0QNlJxN", + watchers: [ + esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, + tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} + ] + +# ## SSL Support +# +# In order to use HTTPS in development, a self-signed +# certificate can be generated by running the following +# Mix task: +# +# mix phx.gen.cert +# +# Run `mix help phx.gen.cert` for more information. +# +# The `http:` config above can be replaced with: +# +# https: [ +# port: 4001, +# cipher_suite: :strong, +# keyfile: "priv/cert/selfsigned_key.pem", +# certfile: "priv/cert/selfsigned.pem" +# ], +# +# If desired, both `http:` and `https:` keys can be +# configured to run both http and https servers on +# different ports. + +# Watch static and templates for browser reloading. +config :app, AppWeb.Endpoint, + live_reload: [ + patterns: [ + ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"priv/gettext/.*(po)$", + ~r"lib/app_web/(live|views)/.*(ex)$", + ~r"lib/app_web/templates/.*(eex)$" + ] + ] + +# Enable dev routes for dashboard and mailbox +config :app, dev_routes: true + +# Do not include metadata nor timestamps in development logs +config :logger, :console, format: "[$level] $message\n" + +# Set a higher stacktrace during development. Avoid configuring such +# in production as building large stacktraces may be expensive. +config :phoenix, :stacktrace_depth, 20 + +# Initialize plugs at runtime for faster development compilation +config :phoenix, :plug_init_mode, :runtime + +# Disable swoosh api client as it is only required for production adapters. +config :swoosh, :api_client, false diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..4eb9e8e --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,21 @@ +import Config + +# For production, don't forget to configure the url host +# to something meaningful, Phoenix uses this information +# when generating URLs. + +# Note we also include the path to a cache manifest +# containing the digested version of static files. This +# manifest is generated by the `mix phx.digest` task, +# which you should run after static files are built and +# before starting your production server. +config :app, AppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" + +# Configures Swoosh API Client +config :swoosh, :api_client, App.Finch + +# Do not print debug messages in production +config :logger, level: :info + +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 0000000..dc49167 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,115 @@ +import Config + +# config/runtime.exs is executed for all environments, including +# during releases. It is executed after compilation and before the +# system starts, so it is typically used to load production configuration +# and secrets from environment variables or elsewhere. Do not define +# any compile-time configuration in here, as it won't be applied. +# The block below contains prod specific runtime configuration. + +# ## Using releases +# +# If you use `mix release`, you need to explicitly enable the server +# by passing the PHX_SERVER=true when you start it: +# +# PHX_SERVER=true bin/app start +# +# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` +# script that automatically sets the env var above. +if System.get_env("PHX_SERVER") do + config :app, AppWeb.Endpoint, server: true +end + +if config_env() == :prod do + database_url = + System.get_env("DATABASE_URL") || + raise """ + environment variable DATABASE_URL is missing. + For example: ecto://USER:PASS@HOST/DATABASE + """ + + maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: [] + + config :app, App.Repo, + # ssl: true, + url: database_url, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + socket_options: maybe_ipv6 + + # The secret key base is used to sign/encrypt cookies and other secrets. + # A default value is used in config/dev.exs and config/test.exs but you + # want to use a different value for prod and you most likely don't want + # to check this value into version control, so we use an environment + # variable instead. + secret_key_base = + System.get_env("SECRET_KEY_BASE") || + raise """ + environment variable SECRET_KEY_BASE is missing. + You can generate one by calling: mix phx.gen.secret + """ + + host = System.get_env("PHX_HOST") || "example.com" + port = String.to_integer(System.get_env("PORT") || "4000") + + config :app, AppWeb.Endpoint, + url: [host: host, port: 443, scheme: "https"], + http: [ + # Enable IPv6 and bind on all interfaces. + # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. + # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html + # for details about using IPv6 vs IPv4 and loopback vs public addresses. + ip: {0, 0, 0, 0, 0, 0, 0, 0}, + port: port + ], + secret_key_base: secret_key_base + + # ## SSL Support + # + # To get SSL working, you will need to add the `https` key + # to your endpoint configuration: + # + # config :app, AppWeb.Endpoint, + # https: [ + # ..., + # port: 443, + # cipher_suite: :strong, + # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), + # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") + # ] + # + # The `cipher_suite` is set to `:strong` to support only the + # latest and more secure SSL ciphers. This means old browsers + # and clients may not be supported. You can set it to + # `:compatible` for wider support. + # + # `:keyfile` and `:certfile` expect an absolute path to the key + # and cert in disk or a relative path inside priv, for example + # "priv/ssl/server.key". For all supported SSL configuration + # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 + # + # We also recommend setting `force_ssl` in your endpoint, ensuring + # no data is ever sent via http, always redirecting to https: + # + # config :app, AppWeb.Endpoint, + # force_ssl: [hsts: true] + # + # Check `Plug.SSL` for all available options in `force_ssl`. + + # ## Configuring the mailer + # + # In production you need to configure the mailer to use a different adapter. + # Also, you may need to configure the Swoosh API client of your choice if you + # are not using SMTP. Here is an example of the configuration: + # + # config :app, App.Mailer, + # adapter: Swoosh.Adapters.Mailgun, + # api_key: System.get_env("MAILGUN_API_KEY"), + # domain: System.get_env("MAILGUN_DOMAIN") + # + # For this example you need include a HTTP client required by Swoosh API client. + # Swoosh supports Hackney and Finch out of the box: + # + # config :swoosh, :api_client, Swoosh.ApiClient.Hackney + # + # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. +end diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..cbea5a8 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,33 @@ +import Config + +# Configure your database +# +# The MIX_TEST_PARTITION environment variable can be used +# to provide built-in test partitioning in CI environment. +# Run `mix help test` for more information. +config :app, App.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "app_test#{System.get_env("MIX_TEST_PARTITION")}", + pool: Ecto.Adapters.SQL.Sandbox, + pool_size: 10 + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :app, AppWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "lrn6ftfPZu7KSTQ54v4foc/gq2FgYfB9/ckQw+Hhi/NNcUM7nf/mUlTqZWJOXAoK", + server: false + +# In test we don't send emails. +config :app, App.Mailer, adapter: Swoosh.Adapters.Test + +# Disable swoosh api client as it is only required for production adapters. +config :swoosh, :api_client, false + +# Print only warnings and errors during test +config :logger, level: :warning + +# Initialize plugs at runtime for faster test compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/lib/app.ex b/lib/app.ex new file mode 100644 index 0000000..a10dc06 --- /dev/null +++ b/lib/app.ex @@ -0,0 +1,9 @@ +defmodule App do + @moduledoc """ + App keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ +end diff --git a/lib/app/application.ex b/lib/app/application.ex new file mode 100644 index 0000000..88c91a8 --- /dev/null +++ b/lib/app/application.ex @@ -0,0 +1,38 @@ +defmodule App.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + # Start the Telemetry supervisor + AppWeb.Telemetry, + # Start the Ecto repository + App.Repo, + # Start the PubSub system + {Phoenix.PubSub, name: App.PubSub}, + # Start Finch + {Finch, name: App.Finch}, + # Start the Endpoint (http/https) + AppWeb.Endpoint + # Start a worker by calling: App.Worker.start_link(arg) + # {App.Worker, arg} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: App.Supervisor] + Supervisor.start_link(children, opts) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + @impl true + def config_change(changed, _new, removed) do + AppWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/lib/app/mailer.ex b/lib/app/mailer.ex new file mode 100644 index 0000000..4c4c2cb --- /dev/null +++ b/lib/app/mailer.ex @@ -0,0 +1,3 @@ +defmodule App.Mailer do + use Swoosh.Mailer, otp_app: :app +end diff --git a/lib/app/repo.ex b/lib/app/repo.ex new file mode 100644 index 0000000..857bd3f --- /dev/null +++ b/lib/app/repo.ex @@ -0,0 +1,5 @@ +defmodule App.Repo do + use Ecto.Repo, + otp_app: :app, + adapter: Ecto.Adapters.Postgres +end diff --git a/lib/app_web.ex b/lib/app_web.ex new file mode 100644 index 0000000..bf82b78 --- /dev/null +++ b/lib/app_web.ex @@ -0,0 +1,114 @@ +defmodule AppWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, components, channels, and so on. + + This can be used in your application as: + + use AppWeb, :controller + use AppWeb, :html + + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define additional modules and import + those modules here. + """ + + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + end + end + + def controller do + quote do + use Phoenix.Controller, + namespace: AppWeb, + formats: [:html, :json], + layouts: [html: AppWeb.Layouts] + + import Plug.Conn + import AppWeb.Gettext + + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {AppWeb.Layouts, :app} + + unquote(html_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(html_helpers()) + end + end + + def html do + quote do + use Phoenix.Component + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(html_helpers()) + end + end + + defp html_helpers do + quote do + # HTML escaping functionality + import Phoenix.HTML + # Core UI components and translation + import AppWeb.CoreComponents + import AppWeb.Gettext + + # Shortcut for generating JS commands + alias Phoenix.LiveView.JS + + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: AppWeb.Endpoint, + router: AppWeb.Router, + statics: AppWeb.static_paths() + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/lib/app_web/components/core_components.ex b/lib/app_web/components/core_components.ex new file mode 100644 index 0000000..f40abef --- /dev/null +++ b/lib/app_web/components/core_components.ex @@ -0,0 +1,623 @@ +defmodule AppWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + + The components in this module use Tailwind CSS, a utility-first CSS framework. + See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to + customize the generated components in this module. + + Icons are provided by [heroicons](https://heroicons.com), using the + [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project. + """ + use Phoenix.Component + + alias Phoenix.LiveView.JS + import AppWeb.Gettext + + @doc """ + Renders a modal. + + ## Examples + + <.modal id="confirm-modal"> + Are you sure? + <:confirm>OK + <:cancel>Cancel + + + JS commands may be passed to the `:on_cancel` and `on_confirm` attributes + for the caller to react to each button press, for example: + + <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}> + Are you sure you? + <:confirm>OK + <:cancel>Cancel + + """ + attr :id, :string, required: true + attr :show, :boolean, default: false + attr :on_cancel, JS, default: %JS{} + attr :on_confirm, JS, default: %JS{} + + slot :inner_block, required: true + slot :title + slot :subtitle + slot :confirm + slot :cancel + + def modal(assigns) do + ~H""" +