Skip to content

Commit 157de86

Browse files
committed
console: Initial work on cloud console app and Stripe. #376
1 parent 79e072c commit 157de86

File tree

11 files changed

+584
-0
lines changed

11 files changed

+584
-0
lines changed

console/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/.cache
2+
/.spago
3+
/node_modules/
4+
/output/
5+
/dist/
6+
/generated-docs/
7+
/.psc-package/
8+
/.psc*
9+
/.purs*
10+
/.psa*

console/html/console.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
background: red;

console/html/index.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@import "@statebox/style/style.min.css";
2+
3+
@import "./console.css";

console/html/index.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!doctype html>
2+
<html xmlns="http://www.w3.org/1999/xhtml" class="stbx-app">
3+
<head>
4+
<meta charset="utf-8"/>
5+
<title>Statebox Cloud Console</title>
6+
<meta name="viewport" content="width=device-width,initial-scale=1">
7+
<link rel="stylesheet" href="index.css">
8+
</head>
9+
<body>
10+
<div id="user"><span id="email"></span><button id="sign-out">Sign Out</button></div>
11+
<div id="firebaseui-auth-container" class="dialog"></div>
12+
13+
<!-- The core Firebase JS SDK is always required and must be listed first -->
14+
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-app.js"></script>
15+
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-auth.js"></script>
16+
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-firestore.js"></script>
17+
<script src="https://www.gstatic.com/firebasejs/7.11.0/firebase-analytics.js"></script>
18+
<script src="https://www.gstatic.com/firebasejs/ui/4.5.0/firebase-ui-auth.js"></script>
19+
<link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/4.5.0/firebase-ui-auth.css" />
20+
21+
<script src="index.js"></script>
22+
</body>
23+
<html>

console/html/index.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
var Main = require("../output/index.js")
2+
3+
////////////////////////////////////////////////////////////////////////////////
4+
//
5+
// initialise Firebase
6+
//
7+
////////////////////////////////////////////////////////////////////////////////
8+
9+
var firebaseConfig = {
10+
apiKey: "AIzaSyAhl4uChdRK_yXiYybtXfqG6uUEk1hAB9A",
11+
authDomain: "statebox-kdmoncat.firebaseapp.com",
12+
databaseURL: "https://statebox-kdmoncat.firebaseio.com",
13+
projectId: "statebox-kdmoncat",
14+
storageBucket: "statebox-kdmoncat.appspot.com",
15+
messagingSenderId: "455902306352",
16+
appId: "1:455902306352:web:6fcdfeb29f583d118d0df5",
17+
measurementId: "G-9FF747MDHW"
18+
}
19+
20+
let firebase = window.firebase
21+
22+
firebase.initializeApp(firebaseConfig)
23+
firebase.analytics()
24+
var db = firebase.firestore()
25+
26+
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
27+
28+
var ui = new firebaseui.auth.AuthUI(firebase.auth())
29+
var uiConfig = {
30+
credentialHelper: firebaseui.auth.CredentialHelper.NONE,
31+
signInFlow: 'popup', // use popup for IDP Providers sign-in flow instead of the default, redirect
32+
signInOptions: [
33+
firebase.auth.EmailAuthProvider.PROVIDER_ID,
34+
],
35+
}
36+
37+
var loggedIn = false
38+
firebase.auth().onAuthStateChanged(function (user) {
39+
if (user) {
40+
start(user)
41+
loggedIn = true
42+
} else {
43+
console.log("firebase auth: not logged in.")
44+
if (!loggedIn) {
45+
ui.start('#firebaseui-auth-container', uiConfig)
46+
} else {
47+
location.reload()
48+
}
49+
}
50+
})
51+
52+
function start (user) {
53+
console.log('user =', user)
54+
document.getElementById('email').innerText = user && user.email || ""
55+
document.getElementById('firebaseui-auth-container').style.display = 'none'
56+
57+
console.log("firebase auth: logged in.")
58+
59+
Main.main()
60+
document.getElementById('sign-out').onclick = function () {
61+
firebase.auth().signOut()
62+
}
63+
}

console/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "stbx-cloud-console",
3+
"version": "1.0.0",
4+
"description": "Statebox Cloud Admin Console",
5+
"main": "index.js",
6+
"directories": {
7+
"test": "test"
8+
},
9+
"scripts": {
10+
"postinstall": "spago install",
11+
"start": "npm run build && concurrently --kill-others --handle-input npm:watch npm:serve",
12+
"build": "spago bundle-module --main Statebox.Console.Main --to output/index.js --purs-args --censor-codes=ImplicitImport,ImplicitQualifiedImport,HidingImport",
13+
"watch": "spago bundle-module --main Statebox.Console.Main --to output/index.js --watch --purs-args --censor-codes=ImplicitImport,ImplicitQualifiedImport,HidingImport",
14+
"test": "spago test",
15+
"docs": "spago docs",
16+
"repl": "spago repl",
17+
"serve": "parcel html/index.html",
18+
"bundle": "npm run build && rm -rf dist && parcel build html/index.html --public-url . --no-source-maps"
19+
},
20+
"keywords": [
21+
"statebox"
22+
],
23+
"author": "Erik Post <[email protected]>",
24+
"license": "Commercial",
25+
"devDependencies": {
26+
"concurrently": "^5.0.2",
27+
"parcel-bundler": "^1.12.4",
28+
"purescript": "^0.13.5",
29+
"purescript-psa": "^0.7.3",
30+
"spago": "^0.13"
31+
},
32+
"dependencies": {
33+
"@statebox/stbx-js": "0.0.31",
34+
"@statebox/style": "0.0.6",
35+
"dagre": "^0.8.4"
36+
}
37+
}

console/spago.dhall

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{ name =
2+
"stbx-cloud-console"
3+
, dependencies =
4+
[ "console", "effect", "halogen", "psci-support" ]
5+
, packages =
6+
../packages.dhall
7+
, sources =
8+
[ "src/**/*.purs", "test/**/*.purs" ]
9+
}

console/src/Statebox/Console.purs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
module Statebox.Console where
2+
3+
import Prelude
4+
import Data.Either (either)
5+
import Data.Lens
6+
import Data.Lens.Record (prop)
7+
import Data.Symbol (SProxy(..))
8+
import Data.Foldable (fold, foldMap)
9+
import Data.Maybe (Maybe(..), maybe, fromMaybe)
10+
import Effect.Aff.Class (class MonadAff)
11+
import Effect.Console (log)
12+
import Halogen as H
13+
import Halogen (ComponentHTML)
14+
import Halogen.HTML (HTML, p, text, div, ul, li, h2, table, tr, th, td)
15+
import Halogen.Query.HalogenM (HalogenM)
16+
17+
import Statebox.Console.DAO as DAO
18+
19+
import Stripe as Stripe
20+
21+
import Debug.Trace (spy)
22+
23+
--------------------------------------------------------------------------------
24+
25+
type State =
26+
{ customer :: Maybe Stripe.Customer
27+
, paymentMethods :: Array Stripe.PaymentMethod
28+
, accounts :: Array { invoices :: Array Stripe.Invoice
29+
}
30+
, status :: AppStatus
31+
}
32+
33+
_accounts = prop (SProxy :: SProxy "accounts")
34+
_invoices = prop (SProxy :: SProxy "invoices")
35+
36+
--------------------------------------------------------------------------------
37+
38+
data AppStatus = Ok | ErrorStatus String
39+
40+
derive instance eqAppStatus :: Eq AppStatus
41+
42+
instance showAppStatus :: Show AppStatus where
43+
show = case _ of
44+
Ok -> "Ok"
45+
ErrorStatus x -> "(ErrorStatus " <> x <> ")"
46+
47+
type Input = State
48+
49+
data Action = FetchStuff
50+
51+
data Query a = DoAction Action a
52+
53+
type ChildSlots = ()
54+
55+
ui :: m. MonadAff m => H.Component HTML Query Input Void m
56+
ui =
57+
H.mkComponent
58+
{ initialState: mkInitialState
59+
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction, handleQuery = handleQuery }
60+
, render: render
61+
}
62+
63+
mkInitialState :: Input -> State
64+
mkInitialState input = input
65+
66+
handleQuery = case _ of
67+
(DoAction x next) -> do
68+
handleAction x
69+
pure (Just next)
70+
71+
handleAction :: m. MonadAff m => Action -> HalogenM State Action ChildSlots Void m Unit
72+
handleAction = case _ of
73+
FetchStuff -> do
74+
H.liftEffect $ log "handling action FetchStuff..."
75+
invoicesEE <- H.liftAff $ DAO.listInvoices
76+
invoicesEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch invoices." })
77+
(either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding invoices failed."})
78+
(\x -> H.modify_ $ _ { accounts = [ { invoices: x.data } ] }))
79+
spyM "invoicesEE" $ invoicesEE
80+
81+
customerEE <- H.liftAff $ DAO.fetchCustomer
82+
customerEE # either (\e -> H.modify_ $ _ { customer = Nothing, status = ErrorStatus "Failed to fetch customer." })
83+
(either (\e -> H.modify_ $ _ { customer = Nothing, status = ErrorStatus "Decoding invoices failed."})
84+
(\x -> H.modify_ $ _ { customer = Just x }))
85+
spyM "customerEE" $ customerEE
86+
87+
paymentMethodsEE <- H.liftAff $ DAO.listPaymentMethods
88+
paymentMethodsEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch payment methods." })
89+
(either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding payment methods failed."})
90+
(\x -> H.modify_ $ _ { paymentMethods = x.data }))
91+
spyM "paymentMethodsEE" $ paymentMethodsEE
92+
93+
H.liftEffect $ log "FetchStuff done."
94+
95+
--------------------------------------------------------------------------------
96+
97+
render :: m. MonadAff m => State -> ComponentHTML Action ChildSlots m
98+
render state =
99+
div []
100+
[ p [] [ text $ if state.status == Ok then "" else "status: " <> show state.status ]
101+
, h2 [] [ text "Customer" ]
102+
, div [] (maybe [] (pure <<< customerHtml) state.customer)
103+
, h2 [] [ text "Invoices" ]
104+
, div []
105+
(state.accounts <#> \account -> table []
106+
(account.invoices <#> invoiceSummaryLineHtml)
107+
)
108+
]
109+
110+
invoiceSummaryLineHtml :: m. MonadAff m => Stripe.Invoice -> ComponentHTML Action ChildSlots m
111+
invoiceSummaryLineHtml i =
112+
tr [] [ td [] [ text $ i.customer_email ]
113+
, td [] [ text $ i.account_name ]
114+
, td [] [ text $ i.currency ]
115+
, td [] [ text $ show i.amount_due ]
116+
]
117+
118+
customerHtml :: m. MonadAff m => Stripe.Customer -> ComponentHTML Action ChildSlots m
119+
customerHtml c =
120+
table []
121+
[ tr [] [ th [] [ text "name" ]
122+
, td [] [ text $ fold c.name ]
123+
]
124+
, tr [] [ th [] [ text "email" ]
125+
, td [] [ text $ c.email ]
126+
]
127+
, tr [] [ th [] [ text "phone" ]
128+
, td [] [ text $ fold c.phone ]
129+
]
130+
, tr [] [ th [] [ text "description" ]
131+
, td [] [ text $ fold c.description ]
132+
]
133+
, tr [] [ th [] [ text "balance" ]
134+
, td [] [ text $ c.currency <> " " <> show c.balance <> " cents" ]
135+
]
136+
]
137+
138+
--------------------------------------------------------------------------------
139+
140+
spyM :: m a. Applicative m => String -> a -> m Unit
141+
spyM tag value = do
142+
let dummy1 = spy tag value
143+
pure unit

console/src/Statebox/Console/DAO.purs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
module Statebox.Console.DAO where
2+
3+
import Prelude
4+
import Affjax as Affjax
5+
import Affjax (Response, URL)
6+
import Affjax.ResponseFormat as ResponseFormat
7+
import Data.Argonaut.Core (Json)
8+
import Data.Argonaut.Decode (decodeJson)
9+
import Data.HTTP.Method (Method(GET))
10+
import Data.Either (Either(..), either)
11+
import Data.Either.Nested (type (\/))
12+
import Effect.Aff (Aff)
13+
14+
import Stripe as Stripe
15+
16+
import Debug.Trace (spy)
17+
18+
mkUrl suffix = "http://localhost" <> suffix
19+
20+
--------------------------------------------------------------------------------
21+
22+
type InvoicesResponse =
23+
{ object :: String
24+
, "data" :: Array Stripe.Invoice
25+
}
26+
27+
listInvoices :: Aff (Affjax.Error \/ String \/ InvoicesResponse)
28+
listInvoices = listInvoices' # map (map (_.body >>> spy "invoices body dump" >>> decodeJson))
29+
30+
listInvoices' :: Aff (Affjax.Error \/ Response Json)
31+
listInvoices' =
32+
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/invoices"
33+
, method = Left GET
34+
, responseFormat = ResponseFormat.json
35+
}
36+
37+
--------------------------------------------------------------------------------
38+
39+
fetchCustomer :: Aff (Affjax.Error \/ String \/ Stripe.Customer)
40+
fetchCustomer = fetchCustomer' # map (map (_.body >>> spy "customer body dump" >>> decodeJson))
41+
42+
fetchCustomer' :: Aff (Affjax.Error \/ Response Json)
43+
fetchCustomer' =
44+
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/customer"
45+
, method = Left GET
46+
, responseFormat = ResponseFormat.json
47+
}
48+
49+
--------------------------------------------------------------------------------
50+
51+
type PaymentMethodsResponse =
52+
{ object :: Stripe.ObjectTag
53+
, "data" :: Array Stripe.PaymentMethod
54+
, has_more :: Boolean
55+
, url :: Stripe.URLSuffix
56+
}
57+
58+
listPaymentMethods :: Aff (Affjax.Error \/ String \/ PaymentMethodsResponse)
59+
listPaymentMethods = listPaymentMethods' # map (map (_.body >>> spy "paymentMethods dump" >>> decodeJson))
60+
61+
listPaymentMethods' :: Aff (Affjax.Error \/ Response Json)
62+
listPaymentMethods' =
63+
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/payment-methods"
64+
, method = Left GET
65+
, responseFormat = ResponseFormat.json
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module Statebox.Console.Main where
2+
3+
import Prelude
4+
import Data.Maybe
5+
import Effect (Effect)
6+
import Halogen as H
7+
import Halogen.Aff (awaitBody, runHalogenAff)
8+
import Halogen.VDom.Driver (runUI)
9+
10+
import Statebox.Console as Console
11+
12+
main :: Effect Unit
13+
main = runHalogenAff do
14+
body <- awaitBody
15+
io <- runUI Console.ui initialState body
16+
_ <- io.query $ H.tell $ Console.DoAction Console.FetchStuff
17+
pure io
18+
where
19+
initialState :: Console.State
20+
initialState = { customer: Nothing
21+
, paymentMethods: mempty
22+
, accounts: [ { invoices: mempty } ]
23+
, status: Console.Ok
24+
}

0 commit comments

Comments
 (0)