Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

console: Initial version. #376 (WIP) #385

Open
wants to merge 15 commits into
base: development
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
console: Initial work on cloud console app and Stripe. #376
  • Loading branch information
epost committed Mar 19, 2020

Verified

This commit was signed with the committer’s verified signature.
TheBigLee Bigli
commit 4b5c2ed7ec9d93201f50066281b6401005a25519
10 changes: 10 additions & 0 deletions console/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/.cache
/.spago
/node_modules/
/output/
/dist/
/generated-docs/
/.psc-package/
/.psc*
/.purs*
/.psa*
1 change: 1 addition & 0 deletions console/html/console.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
background: red;
3 changes: 3 additions & 0 deletions console/html/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import "@statebox/style/style.min.css";

@import "./console.css";
23 changes: 23 additions & 0 deletions console/html/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" class="stbx-app">
<head>
<meta charset="utf-8"/>
<title>Statebox Cloud Console</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="user"><span id="email"></span><button id="sign-out">Sign Out</button></div>
<div id="firebaseui-auth-container" class="dialog"></div>

<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-firestore.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-analytics.js"></script>
<script src="https://www.gstatic.com/firebasejs/ui/4.5.0/firebase-ui-auth.js"></script>
<link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/4.5.0/firebase-ui-auth.css" />

<script src="index.js"></script>
</body>
<html>
63 changes: 63 additions & 0 deletions console/html/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
var Main = require("../output/index.js")

////////////////////////////////////////////////////////////////////////////////
//
// initialise Firebase
//
////////////////////////////////////////////////////////////////////////////////

var firebaseConfig = {
apiKey: "AIzaSyAhl4uChdRK_yXiYybtXfqG6uUEk1hAB9A",
authDomain: "statebox-kdmoncat.firebaseapp.com",
databaseURL: "https://statebox-kdmoncat.firebaseio.com",
projectId: "statebox-kdmoncat",
storageBucket: "statebox-kdmoncat.appspot.com",
messagingSenderId: "455902306352",
appId: "1:455902306352:web:6fcdfeb29f583d118d0df5",
measurementId: "G-9FF747MDHW"
}

let firebase = window.firebase

firebase.initializeApp(firebaseConfig)
firebase.analytics()
var db = firebase.firestore()

firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)

var ui = new firebaseui.auth.AuthUI(firebase.auth())
var uiConfig = {
credentialHelper: firebaseui.auth.CredentialHelper.NONE,
signInFlow: 'popup', // use popup for IDP Providers sign-in flow instead of the default, redirect
signInOptions: [
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
}

var loggedIn = false
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
start(user)
loggedIn = true
} else {
console.log("firebase auth: not logged in.")
if (!loggedIn) {
ui.start('#firebaseui-auth-container', uiConfig)
} else {
location.reload()
}
}
})

function start (user) {
console.log('user =', user)
document.getElementById('email').innerText = user && user.email || ""
document.getElementById('firebaseui-auth-container').style.display = 'none'

console.log("firebase auth: logged in.")

Main.main()
document.getElementById('sign-out').onclick = function () {
firebase.auth().signOut()
}
}
37 changes: 37 additions & 0 deletions console/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "stbx-cloud-console",
"version": "1.0.0",
"description": "Statebox Cloud Admin Console",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"postinstall": "spago install",
"start": "npm run build && concurrently --kill-others --handle-input npm:watch npm:serve",
"build": "spago bundle-module --main Statebox.Console.Main --to output/index.js --purs-args --censor-codes=ImplicitImport,ImplicitQualifiedImport,HidingImport",
"watch": "spago bundle-module --main Statebox.Console.Main --to output/index.js --watch --purs-args --censor-codes=ImplicitImport,ImplicitQualifiedImport,HidingImport",
"test": "spago test",
"docs": "spago docs",
"repl": "spago repl",
"serve": "parcel html/index.html",
"bundle": "npm run build && rm -rf dist && parcel build html/index.html --public-url . --no-source-maps"
},
"keywords": [
"statebox"
],
"author": "Erik Post <erik@shinsetsu.nl>",
"license": "Commercial",
"devDependencies": {
"concurrently": "^5.0.2",
"parcel-bundler": "^1.12.4",
"purescript": "^0.13.5",
"purescript-psa": "^0.7.3",
"spago": "^0.13"
},
"dependencies": {
"@statebox/stbx-js": "0.0.31",
"@statebox/style": "0.0.6",
"dagre": "^0.8.4"
}
}
17 changes: 17 additions & 0 deletions console/spago.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{ name =
"stbx-cloud-console"
, dependencies =
[ "affjax"
, "argonaut"
, "argonaut-codecs"
, "console"
, "debug"
, "effect"
, "halogen"
, "psci-support"
]
, packages =
../packages.dhall
, sources =
[ "src/**/*.purs", "test/**/*.purs" ]
}
143 changes: 143 additions & 0 deletions console/src/Statebox/Console.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
module Statebox.Console where

import Prelude
import Data.Either (either)
import Data.Lens
import Data.Lens.Record (prop)
import Data.Symbol (SProxy(..))
import Data.Foldable (fold, foldMap)
import Data.Maybe (Maybe(..), maybe, fromMaybe)
import Effect.Aff.Class (class MonadAff)
import Effect.Console (log)
import Halogen as H
import Halogen (ComponentHTML)
import Halogen.HTML (HTML, p, text, div, ul, li, h2, table, tr, th, td)
import Halogen.Query.HalogenM (HalogenM)

import Statebox.Console.DAO as DAO

import Stripe as Stripe

import Debug.Trace (spy)

--------------------------------------------------------------------------------

type State =
{ customer :: Maybe Stripe.Customer
, paymentMethods :: Array Stripe.PaymentMethod
, accounts :: Array { invoices :: Array Stripe.Invoice
}
, status :: AppStatus
}

_accounts = prop (SProxy :: SProxy "accounts")
_invoices = prop (SProxy :: SProxy "invoices")

--------------------------------------------------------------------------------

data AppStatus = Ok | ErrorStatus String

derive instance eqAppStatus :: Eq AppStatus

instance showAppStatus :: Show AppStatus where
show = case _ of
Ok -> "Ok"
ErrorStatus x -> "(ErrorStatus " <> x <> ")"

type Input = State

data Action = FetchStuff

data Query a = DoAction Action a

type ChildSlots = ()

ui :: ∀ m. MonadAff m => H.Component HTML Query Input Void m
ui =
H.mkComponent
{ initialState: mkInitialState
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction, handleQuery = handleQuery }
, render: render
}

mkInitialState :: Input -> State
mkInitialState input = input

handleQuery = case _ of
(DoAction x next) -> do
handleAction x
pure (Just next)

handleAction :: ∀ m. MonadAff m => Action -> HalogenM State Action ChildSlots Void m Unit
handleAction = case _ of
FetchStuff -> do
H.liftEffect $ log "handling action FetchStuff..."
invoicesEE <- H.liftAff $ DAO.listInvoices
invoicesEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch invoices." })
(either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding invoices failed."})
(\x -> H.modify_ $ _ { accounts = [ { invoices: x.data } ] }))
spyM "invoicesEE" $ invoicesEE

customerEE <- H.liftAff $ DAO.fetchCustomer
customerEE # either (\e -> H.modify_ $ _ { customer = Nothing, status = ErrorStatus "Failed to fetch customer." })
(either (\e -> H.modify_ $ _ { customer = Nothing, status = ErrorStatus "Decoding customer failed."})
(\x -> H.modify_ $ _ { customer = Just x }))
spyM "customerEE" $ customerEE

paymentMethodsEE <- H.liftAff $ DAO.listPaymentMethods
paymentMethodsEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch payment methods." })
(either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding payment methods failed."})
(\x -> H.modify_ $ _ { paymentMethods = x.data }))
spyM "paymentMethodsEE" $ paymentMethodsEE

H.liftEffect $ log "FetchStuff done."

--------------------------------------------------------------------------------

render :: ∀ m. MonadAff m => State -> ComponentHTML Action ChildSlots m
render state =
div []
[ p [] [ text $ if state.status == Ok then "" else "status: " <> show state.status ]
, h2 [] [ text "Customer" ]
, div [] (maybe [] (pure <<< customerHtml) state.customer)
, h2 [] [ text "Invoices" ]
, div []
(state.accounts <#> \account -> table []
(account.invoices <#> invoiceSummaryLineHtml)
)
]

invoiceSummaryLineHtml :: ∀ m. MonadAff m => Stripe.Invoice -> ComponentHTML Action ChildSlots m
invoiceSummaryLineHtml i =
tr [] [ td [] [ text $ i.customer_email ]
, td [] [ text $ i.account_name ]
, td [] [ text $ i.currency ]
, td [] [ text $ show i.amount_due ]
]

customerHtml :: ∀ m. MonadAff m => Stripe.Customer -> ComponentHTML Action ChildSlots m
customerHtml c =
table []
[ tr [] [ th [] [ text "name" ]
, td [] [ text $ fold c.name ]
]
, tr [] [ th [] [ text "email" ]
, td [] [ text $ c.email ]
]
, tr [] [ th [] [ text "phone" ]
, td [] [ text $ fold c.phone ]
]
, tr [] [ th [] [ text "description" ]
, td [] [ text $ fold c.description ]
]
, tr [] [ th [] [ text "balance" ]
, td [] [ text $ c.currency <> " " <> show c.balance <> " cents" ]
]
]

--------------------------------------------------------------------------------

spyM :: ∀ m a. Applicative m => String -> a -> m Unit
spyM tag value = do
let dummy1 = spy tag value
pure unit
66 changes: 66 additions & 0 deletions console/src/Statebox/Console/DAO.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module Statebox.Console.DAO where

import Prelude
import Affjax as Affjax
import Affjax (Response, URL)
import Affjax.ResponseFormat as ResponseFormat
import Data.Argonaut.Core (Json)
import Data.Argonaut.Decode (decodeJson)
import Data.HTTP.Method (Method(GET))
import Data.Either (Either(..), either)
import Data.Either.Nested (type (\/))
import Effect.Aff (Aff)

import Stripe as Stripe

import Debug.Trace (spy)

mkUrl suffix = "http://localhost" <> suffix

--------------------------------------------------------------------------------

type InvoicesResponse =
{ object :: String
, "data" :: Array Stripe.Invoice
}

listInvoices :: Aff (Affjax.Error \/ String \/ InvoicesResponse)
listInvoices = listInvoices' # map (map (_.body >>> spy "invoices body dump" >>> decodeJson))

listInvoices' :: Aff (Affjax.Error \/ Response Json)
listInvoices' =
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/invoices"
, method = Left GET
, responseFormat = ResponseFormat.json
}

--------------------------------------------------------------------------------

fetchCustomer :: Aff (Affjax.Error \/ String \/ Stripe.Customer)
fetchCustomer = fetchCustomer' # map (map (_.body >>> spy "customer body dump" >>> decodeJson))

fetchCustomer' :: Aff (Affjax.Error \/ Response Json)
fetchCustomer' =
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/customer"
, method = Left GET
, responseFormat = ResponseFormat.json
}

--------------------------------------------------------------------------------

type PaymentMethodsResponse =
{ object :: Stripe.ObjectTag
, "data" :: Array Stripe.PaymentMethod
, has_more :: Boolean
, url :: Stripe.URLSuffix
}

listPaymentMethods :: Aff (Affjax.Error \/ String \/ PaymentMethodsResponse)
listPaymentMethods = listPaymentMethods' # map (map (_.body >>> spy "paymentMethods dump" >>> decodeJson))

listPaymentMethods' :: Aff (Affjax.Error \/ Response Json)
listPaymentMethods' =
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/payment-methods"
, method = Left GET
, responseFormat = ResponseFormat.json
}
24 changes: 24 additions & 0 deletions console/src/Statebox/Console/Main.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Statebox.Console.Main where

import Prelude
import Data.Maybe
import Effect (Effect)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.VDom.Driver (runUI)

import Statebox.Console as Console

main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
io <- runUI Console.ui initialState body
_ <- io.query $ H.tell $ Console.DoAction Console.FetchStuff
pure io
where
initialState :: Console.State
initialState = { customer: Nothing
, paymentMethods: mempty
, accounts: [ { invoices: mempty } ]
, status: Console.Ok
}
Loading