Implement project invitations.

[?]
Feb 9, 2021, 6:02 AM
46PUXHTYRNWQEELXOM7M7NTAFABN5JYQSB5HPN5VF4HWBDQWJU7QC

Dependencies

  • [2] AKM2VYBL Fix errors with project ID persistence.
  • [3] I5MPORH4 Autofill signup form from query string parameters.
  • [4] 7TQPQW3N Begin adding parsing for project detail.
  • [5] QAC2QJ32 Add project overview page to client.
  • [6] I4W76IFV Render recaptcha explicitly.
  • [7] 6L5BK5EH Use generic SMTP rather than Sendmail-specific mail client.
  • [8] U256ZALI Add captcha check to register route.
  • [9] GLQSD33Y Use mock capability for overview init.
  • [10] O2BZOX7M Add signup form, captcha check.
  • [11] QMRKFEPG Refactor QDB to use a free monad algebra instead.
  • [12] FBFDB2ZQ We can render QR codes now.
  • [13] X3ES7NUA Fine. I'll use ormolu. At least it doesn't break the code.
  • [14] NEDDHXUK Reformat via stylish-haskell
  • [15] O5FVTOM6 Undo JSON silliness, enable a couple more routes.
  • [16] NAFJ6RB3 Minor module reorg.
  • [17] Z5KNL332 Add skeleton of project overview HTML.
  • [18] IPG33FAW Add billing daemon
  • [19] M4PWY5RU Preliminary work to add support for Zcash payments.
  • [20] IR75ZMX3 Return actual events for interval ends, not just timestamps.
  • [21] ENNZIQJG Use live signup API for client.
  • [22] YBLHJFCN Implement billing modal.
  • [23] EFSXYZPO Autoformat everything with brittany.
  • [24] T2DN23M7 Factor out billing create component.
  • [25] HMDM3B55 Implement core of payments/billing infrastructure.
  • [26] MJ6R42RC Utility methods for reading key & cert data.
  • [27] H2ABVZI2 Add endpoint for payment request creation.
  • [28] 2XQD6KKK Add invitation logic and clean up DBProg error handling.
  • [29] ZHV75AEN basic cleanup
  • [30] V54JCKJX Payment request creation.
  • [31] DAPLYXHY Successfully rendering QR codes sometimes.
  • [32] SAESJLLY Initial experiments in hash routing.
  • [33] 4354Y4PE Add endpoint to list project contributors.
  • [34] RV7ZIULZ Update overview to have access to the real project detail capability.
  • [35] KET5QGQP Add billable list (in-progress)
  • [36] V2VDN77H Enable postgres configuration via environment variable for Heroku.
  • [37] JFOEOFGA stylish-haskell formatting.
  • [38] QH4UB73N Format with purty.
  • [39] 5IDB3IWS Integrate zcashd-based zaddr validation.
  • [40] U7YAT2ZK Add error reporting to signup form.
  • [41] 5R2Z7FSX Initial rendering for signup controls.
  • [42] PPW6ROC5 Render project data
  • [43] ANDJ6GEY Add billing component skeleton.
  • [44] APOATM4X Add getProjectDetail call to project API
  • [45] XXJFUZOV Add first revenue date to project payout computation.
  • [46] KKJSBWO6 Add createPaymentRequestHandler
  • [*] 3HTCTHHU Add halogen-portal dependency and update argonaut.
  • [*] 4GOBY5NQ WIP on modals.
  • [*] EA5BFM5G Split Login component into its own module.
  • [*] MU6WOCCJ Update auctions to permit zcash as a funding currency.
  • [*] PBD7LZYQ Postgres & auth are beginning to function.
  • [*] ADMKQQGC Initial empty Snap project.

Change contents

  • edit in client/src/Aftok/Api/Project.purs at line 9
    [48.1829]
    [4.3197]
    import Data.Argonaut.Encode (encodeJson)
  • replacement in client/src/Aftok/Api/Project.purs at line 16
    [4.26][4.226:252]()
    import Data.Maybe (Maybe)
    [4.26]
    [4.252]
    import Data.Maybe (Maybe(..))
  • replacement in client/src/Aftok/Api/Project.purs at line 26
    [4.1175][4.1129:1149](),[4.3542][4.1129:1149]()
    import Affjax (get)
    [4.1175]
    [4.3616]
    import Affjax (get, post)
  • edit in client/src/Aftok/Api/Project.purs at line 28
    [4.3651]
    [4.3651]
    import Affjax.RequestBody as RB
  • replacement in client/src/Aftok/Api/Project.purs at line 35
    [4.3726][4.1176:1189]()
    (APIError)
    [4.3726]
    [4.1189]
    (APIError, CommsAddress(..), Zip321Request(..))
  • edit in client/src/Aftok/Api/Project.purs at line 39
    [4.1236]
    [4.1228]
    , parseResponse
  • edit in client/src/Aftok/Api/Project.purs at line 194
    [4.2250]
    encodeInviteBy :: CommsAddress -> Json
    encodeInviteBy = case _ of
    EmailCommsAddr email -> encodeJson ({ email: email })
    ZcashCommsAddr zaddr -> encodeJson ({ zaddr: zaddr })
    type Invitation' by =
    { greetName :: String
    , message :: Maybe String
    , inviteBy :: by
    }
    type Invitation = Invitation' CommsAddress
    encodeInvitation :: Invitation' Json -> Json
    encodeInvitation = encodeJson
    type InvResult =
    { zip321_request :: Maybe String
    }
    decodeInvResult :: Json -> Either JsonDecodeError InvResult
    decodeInvResult = decodeJson
    invite :: ProjectId -> Invitation -> Aff (Either APIError (Maybe Zip321Request))
    invite pid inv = do
    let inv' = inv { inviteBy = encodeInviteBy inv.inviteBy }
    let body = RB.json $ encodeInvitation inv'
    response <- post RF.json ("/api/projects/" <> pidStr pid <> "/invite") (Just body)
    map (\r -> Zip321Request <$> r.zip321_request) <$> parseResponse decodeInvResult response
  • edit in client/src/Aftok/Api/Types.purs at line 7
    [4.5945]
    [4.5945]
    import Data.Newtype (class Newtype)
  • edit in client/src/Aftok/Api/Types.purs at line 24
    [4.452]
    data CommsType
    = EmailComms
    | ZcashComms
    derive instance commsTypeEq :: Eq CommsType
    data CommsAddress
    = EmailCommsAddr String
    | ZcashCommsAddr String
    newtype Zip321Request = Zip321Request String
    derive instance zip321RequestNewtype :: Newtype Zip321Request _
  • edit in client/src/Aftok/Billing/PaymentRequest.purs at line 9
    [2.152]
    [4.3584]
    import Data.Newtype (unwrap)
  • replacement in client/src/Aftok/Billing/PaymentRequest.purs at line 24
    [4.1870][4.1870:1908]()
    import Aftok.Api.Types (APIError(..))
    [4.1870]
    [4.1908]
    import Aftok.Api.Types (APIError(..), Zip321Request(..))
  • edit in client/src/Aftok/Billing/PaymentRequest.purs at line 27
    [4.1948][4.3652:3676]()
    , PaymentRequest'(..)
  • replacement in client/src/Aftok/Billing/PaymentRequest.purs at line 202
    [4.7092][4.542:578]()
    type QrInput = Maybe PaymentRequest
    [4.7092]
    [4.578]
    type QrInput = Maybe Zip321Request
  • replacement in client/src/Aftok/Billing/PaymentRequest.purs at line 205
    [4.595][4.595:627]()
    { req :: Maybe PaymentRequest
    [4.595]
    [4.627]
    { req :: Maybe Zip321Request
  • replacement in client/src/Aftok/Billing/PaymentRequest.purs at line 210
    [4.7144][4.7144:7174]()
    = QrRender PaymentRequest a
    [4.7144]
    [4.7174]
    = QrRender Zip321Request a
  • edit in client/src/Aftok/Billing/PaymentRequest.purs at line 249
    [4.873]
    [4.8010]
    , HH.div_
    [ HH.span
    [ P.classes (ClassName <$> ["code", "zip321uri"]) ]
    (HH.text <<< unwrap <$> U.fromMaybe st.req)
    ]
  • replacement in client/src/Aftok/Billing/PaymentRequest.purs at line 260
    [4.909][4.909:956]()
    H.modify_ (_ { dataUrl = Just dataUrl })
    [4.909]
    [4.8191]
    H.modify_ (_ { req = Just r, dataUrl = Just dataUrl })
  • replacement in client/src/Aftok/Billing/PaymentRequest.purs at line 271
    [4.14505][4.987:1028](),[4.1028][4.8432:8465](),[4.8432][4.8432:8465](),[4.8465][4.1029:1103]()
    renderQR :: PaymentRequest -> m String
    renderQR (PaymentRequest r) =
    system.renderQR { value: r.native_request.zip321_request, size: 300 }
    [4.14505]
    renderQR :: Zip321Request -> m String
    renderQR (Zip321Request r) =
    system.renderQR { value: r, size: 300 }
  • replacement in client/src/Aftok/Billing.purs at line 28
    [4.3430][4.3430:3468]()
    import Aftok.Api.Types (APIError(..))
    [4.3430]
    [4.3468]
    import Aftok.Api.Types (APIError(..), Zip321Request(..))
  • edit in client/src/Aftok/Billing.purs at line 35
    [4.3596]
    [4.3615]
    , PaymentRequest'(..)
  • replacement in client/src/Aftok/Billing.purs at line 230
    [4.10152][4.10152:10258]()
    PaymentRequestCreated req -> do
    lift $ system.log "Created payment request, closing modal."
    [4.10152]
    [4.10258]
    PaymentRequestCreated (PaymentRequest req) -> do
  • edit in client/src/Aftok/Billing.purs at line 232
    [4.10334][4.10334:10390]()
    lift $ system.log "About to show QR code modal"
  • replacement in client/src/Aftok/Billing.purs at line 233
    [4.10468][4.10468:10553]()
    _ <- H.query _showPaymentRequest unit $ H.tell (PaymentRequest.QrRender req)
    [4.10468]
    [4.10553]
    let req' = Zip321Request req.native_request.zip321_request
    _ <- H.query _showPaymentRequest unit $ H.tell (PaymentRequest.QrRender req')
  • file addition: Forms.purs (----------)
    [49.3158]
    module Aftok.HTML.Forms where
    import Prelude
    import Data.Maybe (Maybe(..), fromMaybe)
    import Halogen.HTML.Core (AttrName(..), ClassName(..))
    import Halogen.HTML as HH
    import Halogen.HTML.CSS as CSS
    import Halogen.HTML.Events as E
    import Halogen.HTML.Properties as P
    import CSS.Display (display, flex)
    import CSS.Flexbox (flexFlow, row, nowrap)
    import Aftok.Api.Types (CommsType(..))
    type CommsState r =
    { recoveryType :: CommsType
    , recoveryEmail :: Maybe String
    , recoveryZAddr :: Maybe String
    | r }
    type SetCommsType action = CommsType -> action
    type SetEmail action = String -> action
    type SetZaddr action = String -> action
    commsSwitch :: forall i a. SetCommsType a -> CommsType -> HH.HTML i a
    commsSwitch setCommsType rt =
    HH.div
    [ P.classes (ClassName <$> [ "form-group", "mb-3" ]) ]
    [ HH.label
    [ P.for "commsSwitch" ]
    [ HH.text "Choose a communications method" ]
    , HH.div
    [ P.classes (ClassName <$> [ "form-group", "mb-3" ])
    , CSS.style do
    display flex
    flexFlow row nowrap
    ]
    [ HH.span
    [ P.classes (ClassName <$> [ if rt == EmailComms then "text-success" else "text-muted" ]) ]
    $ [ HH.text "Email" ]
    , HH.div
    [ P.classes (ClassName <$> [ "custom-control", "custom-switch", "custom-switch-light", "mx-3" ]) ]
    [ HH.input
    [ P.type_ P.InputCheckbox
    , P.classes (ClassName <$> [ "custom-control-input" ])
    , P.id_ "commsSwitch"
    , P.checked (rt == ZcashComms)
    , E.onChecked (\b -> Just <<< setCommsType $ if b then ZcashComms else EmailComms)
    ]
    , HH.label [ P.classes (ClassName <$> [ "custom-control-label" ]), P.for "commsSwitch" ] []
    ]
    , HH.span
    [ P.classes (ClassName <$> [ if rt == ZcashComms then "text-success" else "text-muted" ]) ]
    [ HH.text "Z-Address" ]
    ]
    ]
    type CommsErrors i a = CommsType -> Array (HH.HTML i a)
    commsField ::
    forall i a r.
    SetEmail a ->
    SetZaddr a ->
    CommsState r ->
    CommsErrors i a ->
    HH.HTML i a
    commsField setEmail setZAddr st errs = case st.recoveryType of
    EmailComms ->
    HH.div
    [ P.id_ "recoveryEmail" ]
    $ [ HH.label [ P.for "email" ] [ HH.text "Email Address" ]
    , HH.input
    [ P.type_ P.InputEmail
    , P.classes (ClassName <$> [ "form-control" ])
    , P.id_ "email"
    , P.placeholder "name@address.com"
    , P.value (fromMaybe "" st.recoveryEmail)
    , E.onValueInput (Just <<< setEmail)
    ]
    ]
    <> errs EmailComms
    ZcashComms ->
    HH.div
    [ P.id_ "recoveryZAddr" ]
    $ [ HH.label
    [ P.for "zaddr" ]
    [ HH.text "Zcash Shielded Address"
    , HH.a
    [ P.attr (AttrName "data-toggle") "modal"
    , P.href "#modalAboutZAddr"
    ]
    [ HH.img [ P.src "/assets/img/icons/duotone-icons/Code/Info-circle.svg" ]
    ]
    ]
    , HH.input
    [ P.type_ P.InputText
    , P.classes (ClassName <$> [ "form-control" ])
    , P.id_ "email"
    , P.placeholder "Enter a Zcash shielded address"
    , P.value (fromMaybe "" st.recoveryZAddr)
    , E.onValueInput (Just <<< setZAddr)
    ]
    ]
    <> errs ZcashComms
  • edit in client/src/Aftok/Overview.purs at line 26
    [4.1519]
    [4.6406]
    import Aftok.Billing.PaymentRequest as PaymentRequest
    import Aftok.Modals as Modals
    import Aftok.Modals.ModalFFI as ModalFFI
  • edit in client/src/Aftok/Overview.purs at line 30
    [4.6446]
    [4.255]
    import Aftok.Projects.Invite as Invite
  • replacement in client/src/Aftok/Overview.purs at line 32
    [4.315][4.37:71]()
    import Aftok.Api.Types (APIError)
    [4.315]
    [4.3542]
    import Aftok.Api.Types (APIError, Zip321Request)
  • edit in client/src/Aftok/Overview.purs at line 59
    [4.2395]
    [4.2395]
    | InvitationCreated (Maybe Zip321Request)
  • edit in client/src/Aftok/Overview.purs at line 66
    [4.6592]
    [4.9890]
    , invitationModal :: Invite.Slot Unit
    , inviteQRModal :: PaymentRequest.QrSlot Unit
  • edit in client/src/Aftok/Overview.purs at line 71
    [4.2567]
    [4.2567]
    _invitationModal = SProxy :: SProxy "invitationModal"
    _inviteQRModal = SProxy :: SProxy "inviteQRModal"
  • edit in client/src/Aftok/Overview.purs at line 76
    [4.175]
    [4.9939]
    , invitationCaps :: Invite.Capability m
  • edit in client/src/Aftok/Overview.purs at line 168
    [4.5905]
    [4.5905]
    <>
    [ HH.div
    [ P.classes (ClassName <$> [ "row", "pt-3", "font-weight-bold" ]) ]
    [ HH.div
    [ P.classes (ClassName <$> [ "col-md-2" ]) ]
    [ Modals.modalButton Invite.modalId "Invite a collaborator" Nothing]
    , system.portal
    _invitationModal
    unit
    (Invite.component system caps.invitationCaps)
    project.projectId
    Nothing
    (Just <<< InvitationCreated)
    , system.portal
    _inviteQRModal
    unit
    (PaymentRequest.qrcomponent system)
    Nothing
    Nothing
    (const Nothing)
    ]
    ]
  • edit in client/src/Aftok/Overview.purs at line 222
    [4.651][4.6533:11215](),[4.3320][4.6533:11215]()
    -- </section>
    -- <!-- Map payouts -->
    -- <div class="row font-weight-bold">
    -- <div class="col-md-2">
    -- </div>
    -- <div class="col-md-4">
    -- Payments
    -- </div>
    -- <div class="col-md-6">
    --
    -- </div>
    -- </div>
    -- <div class="row">
    -- <div class="col-md-2">
    -- </div>
    -- <div class="col-md-2">
    -- Oct 20 2020
    -- </div>
    -- <div class="col-md-2">
    -- 100 zec
    -- </div>
    -- <div class="col-md-2">
    -- Acme PaidUsRight
    -- </div>
    -- <div class="col-md-4">
    -- </div>
    -- </div>
    -- <!-- map payout creditTos-->
    -- <div class="row pt-3">
    -- <div class="col-md-4">
    -- </div>
    -- <div class="col-md-2">
    -- Freuline Fred
    -- </div>
    -- <div class="col-md-2">
    -- 2.4 zec
    -- </div>
    -- <div class="col-md-2">
    -- 2.4 %
    -- </div>
    -- <div class="col-md-2">
    -- </div>
    -- </div>
    -- <div class="row pt-3">
    -- <div class="col-md-4">
    -- </div>
    -- <div class="col-md-2">
    -- Goobie Works A Lot
    -- </div>
    -- <div class="col-md-2">
    -- 50 zec
    -- </div>
    -- <div class="col-md-2">
    -- 50 %
    -- </div>
    -- <div class="col-md-2">
    -- </div>
    -- </div> <div class="row pt-3">
    -- <div class="col-md-4">
    -- </div>
    -- <div class="col-md-2">
    -- Average Fella
    -- </div>
    -- <div class="col-md-2">
    -- 25 zec
    -- </div>
    -- <div class="col-md-2">
    -- 25 %
    -- </div>
    -- <div class="col-md-2">
    -- </div>
    -- </div> <div class="row pt-3">
    -- <div class="col-md-4">
    -- </div>
    -- <div class="col-md-2">
    -- Cool Kid
    -- </div>
    -- <div class="col-md-2">
    -- 24.6 zec
    -- </div>
    -- <div class="col-md-2">
    -- 24.6 %
    -- </div>
    -- <div class="col-md-2">
    -- </div>
    -- </div>
    --
    -- </section>
    --
    --
    -- <!-- New Project form-->
    -- <section id="addProject">
    --
    -- <div class="row pt-3">
    -- <div class="col-md-4">
    -- <span class="float-right">Project Name</span>
    -- </div>
    -- <div class="col-md-4">
    -- <input type="text" id="projectName" name="projectName" />
    -- </div>
    -- </div>
    --
    -- <div class="row pt-3">
    -- <div class="col-md-4">
    -- <span class="float-right">Undepreciated Period ( Months )</span>
    -- </div>
    -- <div class="col-md-4">
    -- <input type="text" id="undepreciatedPeriod" name="undepreciatedPeriod" />
    -- </div>
    -- </div>
    --
    -- <div class="row pt-3">
    -- <div class="col-md-4">
    -- <span class="float-right">Depreciation Duration ( Months )</span>
    -- </div>
    -- <div class="col-md-4">
    -- <input type="text" id="depreciationDuration" name="depreciationDuration" />
    -- </div>
    -- </div>
    --
    -- <div class="row pt-3 pb-3">
    -- <div class="col-md-2">
    -- </div>
    -- <div class="col-md-10">
    -- <button class="btn btn-sm btn-primary lift ml-auto">Add Project</button>
    -- </div>
    -- </div>
    --
    -- </section>
  • edit in client/src/Aftok/Overview.purs at line 237
    [4.680]
    [4.4104]
    InvitationCreated req -> do
    lift $ system.toggleModal Invite.modalId ModalFFI.HideModal
    lift $ system.toggleModal PaymentRequest.qrModalId ModalFFI.ShowModal
    traverse_ (\r -> H.query _inviteQRModal unit $ H.tell (PaymentRequest.QrRender r)) req
    pure unit
  • edit in client/src/Aftok/Overview.purs at line 253
    [4.345]
    [4.6925]
    , invitationCaps: Invite.apiCapability
  • edit in client/src/Aftok/Overview.purs at line 282
    [4.12245]
    [4.6985]
    , invitationCaps: Invite.apiCapability
  • file addition: Projects (d--r------)
    [50.1]
  • file addition: Invite.purs (----------)
    [0.7504]
    module Aftok.Projects.Invite where
    import Prelude
    import Control.Monad.Trans.Class (lift)
    import Data.Array (filter)
    import Data.Either (Either(..), note)
    import Data.Foldable (any)
    import Data.Maybe (Maybe(..))
    import Data.Validation.Semigroup (V(..), toEither)
    import Effect.Aff (Aff)
    import Halogen as H
    import Halogen.HTML as HH
    import Halogen.HTML.Core (ClassName(..))
    import Halogen.HTML.Events as E
    import Halogen.HTML.Properties as P
    import Aftok.Api.Account as Acc
    import Aftok.Api.Project as Project
    import Aftok.Api.Project (Invitation')
    import Aftok.Api.Types (APIError, CommsType(..), CommsAddress(..), Zip321Request)
    import Aftok.HTML.Forms (commsSwitch, commsField)
    import Aftok.HTML.Classes as C
    import Aftok.Modals as Modals
    import Aftok.Modals.ModalFFI as ModalFFI
    import Aftok.Types (System, ProjectId)
    data Field
    = NameField
    | EmailField
    | ZAddrField
    derive instance fieldEq :: Eq Field
    derive instance fieldOrd :: Ord Field
    type CState =
    { projectId :: ProjectId
    , greetName :: Maybe String
    , message :: Maybe String
    , recoveryType :: CommsType
    , recoveryEmail :: Maybe String
    , recoveryZAddr :: Maybe String
    , fieldErrors :: Array Field
    }
    type Input = ProjectId
    type Output = Maybe Zip321Request
    data Action
    = ProjectChanged ProjectId
    | SetGreetName String
    | SetMessage String
    | SetCommsType CommsType
    | SetEmail String
    | SetZAddr String
    | CreateInvitation
    type Slot id
    = forall query. H.Slot query Output id
    type Capability (m :: Type -> Type)
    = { createInvitation :: ProjectId -> Invitation' CommsAddress -> m (Either APIError (Maybe Zip321Request))
    , checkZAddr :: String -> m Acc.ZAddrCheckResponse
    }
    modalId :: String
    modalId = "createInvitation"
    component ::
    forall query m.
    Monad m =>
    System m ->
    Capability m ->
    H.Component HH.HTML query Input Output m
    component system caps =
    H.mkComponent
    { initialState
    , render
    , eval:
    H.mkEval
    $ H.defaultEval
    { handleAction = eval
    , receive = Just <<< ProjectChanged
    }
    }
    where
    initialState :: Input -> CState
    initialState input =
    { projectId: input
    , greetName : Nothing
    , message : Nothing
    , recoveryType: EmailComms
    , recoveryEmail: Nothing
    , recoveryZAddr: Nothing
    , fieldErrors : []
    }
    render :: forall slots. CState -> H.ComponentHTML Action slots m
    render st =
    Modals.modalWithSave modalId "Invite a collaborator" CreateInvitation
    [ HH.form_
    [ formGroup st
    [ NameField ]
    [ HH.label
    [ P.for "greetName"]
    [ HH.text "Name" ]
    , HH.input
    [ P.type_ P.InputText
    , P.classes [ C.formControl, C.formControlSm ]
    , P.id_ "greetName"
    , P.placeholder "Who are you inviting?"
    , E.onValueInput (Just <<< SetGreetName)
    ]
    ]
    , formGroup st
    [ ]
    [ HH.label
    [ P.for "message"]
    [ HH.text "Message" ]
    , HH.input
    [ P.type_ P.InputText
    , P.classes [C.formControl, C.formControlSm]
    , P.id_ "message"
    , P.placeholder "Enter your message here"
    , E.onValueInput (Just <<< SetMessage)
    ]
    ]
    , commsSwitch SetCommsType st.recoveryType
    , commsField SetEmail SetZAddr st $ case _ of
    EmailComms -> fieldError st EmailField
    ZcashComms -> fieldError st ZAddrField
    ]
    ]
    formGroup :: forall i a. CState -> Array Field -> Array (HH.HTML i a) -> HH.HTML i a
    formGroup st fields body =
    HH.div
    [ P.classes [C.formGroup] ]
    (body <> (fieldError st =<< fields))
    fieldError :: forall i a. CState -> Field -> Array (HH.HTML i a)
    fieldError st field =
    if any (_ == field) st.fieldErrors
    then case field of
    NameField -> err "The name field is required"
    EmailField -> err "The email field is when email comms are selected"
    ZAddrField -> err "Not a valid Zcash shielded address."
    else []
    where
    err str = [ HH.div_ [ HH.span [ P.classes (ClassName <$> [ "badge", "badge-danger-soft" ]) ] [ HH.text str ] ] ]
    setZAddr addr = do
    zres <- lift $ caps.checkZAddr addr
    H.modify_ (_ { recoveryZAddr = Just addr })
    case zres of
    Acc.ZAddrCheckValid ->
    H.modify_ (\st -> st { fieldErrors = filter (_ /= ZAddrField) st.fieldErrors, recoveryType = ZcashComms })
    Acc.ZAddrCheckInvalid ->
    H.modify_ (\st -> st { fieldErrors = st.fieldErrors <> [ZAddrField] })
    -- eval :: forall slots. Action -> H.HalogenM CState Action slots Output m Unit
    eval = case _ of
    ProjectChanged pid ->
    H.modify_ (_ { projectId = pid })
    SetGreetName name ->
    H.modify_ (_ { greetName = Just name })
    SetMessage msg ->
    H.modify_ (_ { message = Just msg })
    SetCommsType t ->
    H.modify_ (_ { recoveryType = t })
    SetEmail email ->
    H.modify_ (_ { recoveryEmail = Just email })
    SetZAddr addr ->
    when (addr /= "") (setZAddr addr)
    CreateInvitation -> do
    nameV <- V <<< note [NameField] <$> H.gets (_.greetName)
    message <- H.gets (_.message)
    addrType <- H.gets (_.recoveryType)
    addrV <-
    case addrType of
    EmailComms -> map EmailCommsAddr <<< V <<< note [EmailField] <$> H.gets (_.recoveryEmail)
    ZcashComms -> map ZcashCommsAddr <<< V <<< note [ZAddrField] <$> H.gets (_.recoveryZAddr)
    let reqV :: V (Array Field) (Invitation' CommsAddress)
    reqV = { greetName: _, message: _, inviteBy: _ }
    <$> nameV
    <*> pure message
    <*> addrV
    case toEither reqV of
    Left errors -> do
    H.modify_ (_ { fieldErrors = errors })
    Right invitation -> do
    pid <- H.gets (_.projectId)
    res <- lift $ caps.createInvitation pid invitation
    case res of
    Right result -> do
    H.raise result
    lift $ system.toggleModal modalId ModalFFI.HideModal
    Left errs ->
    lift $ system.error (show errs)
    apiCapability :: Capability Aff
    apiCapability
    = { createInvitation: Project.invite
    , checkZAddr: Acc.checkZAddr
    }
  • edit in client/src/Aftok/Signup.purs at line 18
    [3.1583][4.131:172](),[4.5168][4.131:172](),[4.131][4.131:172]()
    -- import Affjax (post, get, printError)
  • edit in client/src/Aftok/Signup.purs at line 19
    [4.210][4.210:283]()
    -- import Affjax.RequestBody as RB
    -- import Affjax.ResponseFormat as RF
  • replacement in client/src/Aftok/Signup.purs at line 20
    [4.304][4.304:359]()
    import Halogen.HTML.Core (AttrName(..), ClassName(..))
    [4.304]
    [4.359]
    import Halogen.HTML.Core (ClassName(..))
  • edit in client/src/Aftok/Signup.purs at line 22
    [4.385][4.726:757]()
    import Halogen.HTML.CSS as CSS
  • edit in client/src/Aftok/Signup.purs at line 25
    [4.553][4.553:590](),[4.590][4.758:836]()
    -- import CSS (backgroundImage, url)
    import CSS.Display (display, flex)
    import CSS.Flexbox (flexFlow, row, nowrap)
  • edit in client/src/Aftok/Signup.purs at line 30
    [4.193]
    [3.1658]
    import Aftok.Api.Types (CommsType(..))
    import Aftok.HTML.Forms (commsSwitch, commsField)
  • edit in client/src/Aftok/Signup.purs at line 60
    [4.5575][4.707:813](),[4.12530][4.707:813](),[4.707][4.707:813]()
    data RecoveryType
    = RecoveryEmail
    | RecoveryZAddr
    derive instance recoveryTypeEq :: Eq RecoveryType
  • replacement in client/src/Aftok/Signup.purs at line 65
    [4.14604][4.14604:14639]()
    , recoveryType :: RecoveryType
    [4.14604]
    [4.14639]
    , recoveryType :: CommsType
  • replacement in client/src/Aftok/Signup.purs at line 77
    [4.1166][4.1166:1199]()
    | SetRecoveryType RecoveryType
    [4.1166]
    [4.1199]
    | SetRecoveryType CommsType
  • replacement in client/src/Aftok/Signup.purs at line 121
    [4.15598][4.15598:15632]()
    , recoveryType: RecoveryEmail
    [4.15598]
    [4.15632]
    , recoveryType: EmailComms
  • replacement in client/src/Aftok/Signup.purs at line 165
    [4.13419][4.13419:13478]()
    <> signupErrors UsernameField st
    [4.13419]
    [4.17654]
    <> signupErrors st UsernameField
  • replacement in client/src/Aftok/Signup.purs at line 179
    [4.14118][4.14118:14177]()
    <> signupErrors PasswordField st
    [4.14118]
    [4.14177]
    <> signupErrors st PasswordField
  • replacement in client/src/Aftok/Signup.purs at line 190
    [4.14752][4.14752:14810](),[4.14810][4.18901:18997](),[4.18901][4.18901:18997]()
    <> signupErrors ConfirmField st
    , recoverySwitch st.recoveryType
    , recoveryField st
    [4.14752]
    [3.1838]
    <> signupErrors st ConfirmField
    , commsSwitch SetRecoveryType st.recoveryType
    , commsField SetRecoveryEmail SetRecoveryZAddr st $
    case _ of
    EmailComms -> signupErrors st EmailField
    ZcashComms -> signupErrors st ZAddrField
  • replacement in client/src/Aftok/Signup.purs at line 207
    [3.2493][3.2493:2552]()
    ] <> signupErrors InvCodesField st
    [3.2493]
    [4.18997]
    ] <> signupErrors st InvCodesField
  • replacement in client/src/Aftok/Signup.purs at line 232
    [3.2709][3.2709:2824]()
    H.modify_ (\st -> st { signupErrors = M.delete ZAddrField st.signupErrors, recoveryType = RecoveryZAddr })
    [3.2709]
    [3.2824]
    H.modify_ (\st -> st { signupErrors = M.delete ZAddrField st.signupErrors, recoveryType = ZcashComms })
  • replacement in client/src/Aftok/Signup.purs at line 272
    [4.15580][4.20758:20893](),[4.20758][4.20758:20893]()
    SetRecoveryType t -> H.modify_ (_ { recoveryType = t })
    SetRecoveryEmail email -> H.modify_ (_ { recoveryEmail = Just email })
    [4.15580]
    [4.15581]
    SetRecoveryType t ->
    H.modify_ (_ { recoveryType = t })
    SetRecoveryEmail email ->
    H.modify_ (_ { recoveryEmail = Just email })
  • replacement in client/src/Aftok/Signup.purs at line 289
    [4.21767][4.21767:21987]()
    RecoveryEmail -> V <<< note [ EmailRequired ] <<< map Acc.RecoverByEmail <$> H.gets (_.recoveryEmail)
    RecoveryZAddr -> V <<< note [ ZAddrRequired ] <<< map Acc.RecoverByZAddr <$> H.gets (_.recoveryZAddr)
    [4.21767]
    [4.21987]
    EmailComms -> V <<< note [ EmailRequired ] <<< map Acc.RecoverByEmail <$> H.gets (_.recoveryEmail)
    ZcashComms -> V <<< note [ ZAddrRequired ] <<< map Acc.RecoverByZAddr <$> H.gets (_.recoveryZAddr)
  • replacement in client/src/Aftok/Signup.purs at line 333
    [4.17219][4.17219:17360]()
    signupErrors :: forall i a. SignupField -> SignupState -> Array (HH.HTML i a)
    signupErrors field st = case M.lookup field st.signupErrors of
    [4.17219]
    [4.17360]
    signupErrors :: forall i a. SignupState -> SignupField -> Array (HH.HTML i a)
    signupErrors st field = case M.lookup field st.signupErrors of
  • edit in client/src/Aftok/Signup.purs at line 348
    [4.6158][4.6158:6225](),[4.6225][4.23216:23236](),[4.23236][4.6246:6255](),[4.6246][4.6246:6255](),[4.6255][4.23237:23393](),[4.23393][4.6407:6420](),[4.6407][4.6407:6420](),[4.6420][4.23394:23670](),[4.23670][4.18015:18049](),[4.18049][4.23702:24007](),[4.23702][4.23702:24007](),[4.24007][3.3753:3803](),[3.3803][4.24007:24415](),[4.24007][4.24007:24415](),[4.1257][4.7015:7025](),[4.24415][4.7015:7025](),[4.7015][4.7015:7025](),[4.7184][4.7184:7299](),[4.7299][4.24416:24446](),[4.24446][4.7331:7363](),[4.7331][4.7331:7363](),[4.7363][4.18050:18474](),[4.18474][4.7716:7735](),[4.7716][4.7716:7735](),[4.7735][4.24745:24756](),[4.24756][4.7747:7779](),[4.7747][4.7747:7779](),[4.7779][4.18475:19204](),[4.19204][4.2735:2736](),[4.8397][4.2735:2736]()
    recoverySwitch :: forall i. RecoveryType -> HH.HTML i SignupAction
    recoverySwitch rt =
    HH.div
    [ P.classes (ClassName <$> [ "form-group", "mb-3" ]) ]
    [ HH.label
    [ P.for "recoverySwitch" ]
    [ HH.text "Choose a recovery method" ]
    , HH.div
    [ P.classes (ClassName <$> [ "form-group", "mb-3" ])
    , CSS.style do
    display flex
    flexFlow row nowrap
    ]
    [ HH.span
    [ P.classes (ClassName <$> [ if rt == RecoveryEmail then "text-success" else "text-muted" ]) ]
    $ [ HH.text "Email" ]
    , HH.div
    [ P.classes (ClassName <$> [ "custom-control", "custom-switch", "custom-switch-light", "mx-3" ]) ]
    [ HH.input
    [ P.type_ P.InputCheckbox
    , P.classes (ClassName <$> [ "custom-control-input" ])
    , P.id_ "recoverySwitch"
    , P.checked (rt == RecoveryZAddr)
    , E.onChecked (\b -> Just <<< SetRecoveryType $ if b then RecoveryZAddr else RecoveryEmail)
    ]
    , HH.label [ P.classes (ClassName <$> [ "custom-control-label" ]), P.for "recoverySwitch" ] []
    ]
    , HH.span
    [ P.classes (ClassName <$> [ if rt == RecoveryZAddr then "text-success" else "text-muted" ]) ]
    [ HH.text "Z-Address" ]
    ]
    ]
    recoveryField :: forall i. SignupState -> HH.HTML i SignupAction
    recoveryField st = case st.recoveryType of
    RecoveryEmail ->
    HH.div
    [ P.id_ "recoveryEmail" ]
    $ [ HH.label [ P.for "email" ] [ HH.text "Email Address" ]
    , HH.input
    [ P.type_ P.InputEmail
    , P.classes (ClassName <$> [ "form-control" ])
    , P.id_ "email"
    , P.placeholder "name@address.com"
    , P.value (fromMaybe "" st.recoveryEmail)
    , E.onValueInput (Just <<< SetRecoveryEmail)
    ]
    ]
    <> signupErrors EmailField st
    RecoveryZAddr ->
    HH.div
    [ P.id_ "recoveryZAddr" ]
    $ [ HH.label
    [ P.for "zaddr" ]
    [ HH.text "Zcash Shielded Address"
    , HH.a
    [ P.attr (AttrName "data-toggle") "modal"
    , P.href "#modalAboutZAddr"
    ]
    [ HH.img [ P.src "/assets/img/icons/duotone-icons/Code/Info-circle.svg" ]
    ]
    ]
    , HH.input
    [ P.type_ P.InputText
    , P.classes (ClassName <$> [ "form-control" ])
    , P.id_ "email"
    , P.placeholder "Enter a Zcash shielded address"
    , P.value (fromMaybe "" st.recoveryZAddr)
    , E.onValueInput (Just <<< SetRecoveryZAddr)
    ]
    ]
    <> signupErrors ZAddrField st
  • edit in daemon/AftokD/AftokM.hs at line 26
    [4.1181][4.1181:1224]()
    import qualified Aftok.Payments.Types as P
  • edit in daemon/AftokD/AftokM.hs at line 27
    [4.527]
    [4.1012]
    import qualified Aftok.Payments.Types as P
  • edit in lib/Aftok/Config.hs at line 6
    [4.1218][4.1218:1253]()
    import Aftok.Project (projectName)
  • replacement in lib/Aftok/Config.hs at line 8
    [4.1314][4.1314:1356](),[4.1356][4.14989:15034]()
    import Aftok.Currency.Zcash (Zatoshi(..))
    import Aftok.Currency.Zcash.Types (Memo(..))
    [4.1314]
    [4.1356]
    import Aftok.Currency.Zcash (Zatoshi (..))
    import Aftok.Currency.Zcash.Types (Memo (..))
  • replacement in lib/Aftok/Config.hs at line 11
    [4.1408][4.1408:1451]()
    import Aftok.Payments (PaymentsConfig(..))
    [4.1408]
    [4.6323]
    import Aftok.Payments (PaymentsConfig (..))
  • edit in lib/Aftok/Config.hs at line 14
    [4.1499]
    [4.15035]
    import Aftok.Project (projectName)
  • replacement in lib/Aftok/Config.hs at line 93
    [4.2382][4.2382:2467]()
    { _bitcoinConfig :: BitcoinConfig
    , _zcashConfig :: Zcash.PaymentsConfig
    [4.2382]
    [4.2467]
    { _bitcoinConfig :: BitcoinConfig,
    _zcashConfig :: Zcash.PaymentsConfig
  • replacement in lib/Aftok/Config.hs at line 141
    [4.15373][4.3273:3369](),[4.3273][4.3273:3369](),[4.3369][4.15374:15412](),[4.15412][4.3369:3419](),[4.3369][4.3369:3419]()
    pure $ PaymentsConfig {
    _bitcoinBillingOps = btcOps,
    _bitcoinPaymentsConfig = btcCfg,
    _zcashBillingOps = _zcashMemoGen,
    _zcashPaymentsConfig = cfg ^. zcashConfig
    }
    [4.15373]
    [4.3419]
    pure $
    PaymentsConfig
    { _bitcoinBillingOps = btcOps,
    _bitcoinPaymentsConfig = btcCfg,
    _zcashBillingOps = _zcashMemoGen,
    _zcashPaymentsConfig = cfg ^. zcashConfig
    }
  • edit in lib/Aftok/Config.hs at line 180
    [4.15732][4.15732:15733]()
  • edit in lib/Aftok/Config.hs at line 188
    [4.4131][4.4131:4132]()
  • replacement in lib/Aftok/Config.hs at line 194
    [4.4274][4.4274:4314]()
    Bitcoin.PaymentKey
    -> m (Maybe URI)
    [4.4274]
    [4.4314]
    Bitcoin.PaymentKey ->
    m (Maybe URI)
  • edit in lib/Aftok/Config.hs at line 199
    [4.4481][4.15735:15736]()
  • edit in lib/Aftok/Currency/Zcash.hs at line 15
    [51.1786]
    [4.11323]
    Z.Memo (..),
  • replacement in lib/Aftok/Payments/Util.hs at line 20
    [4.80100][4.16754:16796]()
    import Aftok.Types (ProjectId, AccountId)
    [4.80100]
    [4.80131]
    import Aftok.Types (AccountId, ProjectId)
  • replacement in lib/Aftok/Payments/Zcash.hs at line 13
    [4.82853][4.16909:16954]()
    import Aftok.Currency.Zcash.Types (Memo(..))
    [4.82853]
    [4.82853]
    import Aftok.Currency.Zcash.Types (Memo (..))
  • replacement in lib/Aftok/Payments/Zcash.hs at line 75
    [4.18117][4.18117:18142](),[4.18142][4.84386:84507](),[4.84386][4.84386:84507](),[4.84507][4.18143:18167](),[4.18167][4.84597:84629](),[4.84597][4.84597:84629]()
    pure $ PaymentItem
    { _address = a,
    _label = Nothing,
    _message = billable ^. messageText,
    _amount = z,
    _memo = memo,
    _other = []
    }
    [4.18117]
    pure $
    PaymentItem
    { _address = a,
    _label = Nothing,
    _message = billable ^. messageText,
    _amount = z,
    _memo = memo,
    _other = []
    }
  • edit in server/Aftok/ServerConfig.hs at line 27
    [4.6910][4.4838:4839](),[4.7028][4.4838:4839](),[4.56477][4.4838:4839](),[4.4838][4.4838:4839]()
  • edit in server/Aftok/Snaplet/Billing.hs at line 25
    [4.100419][4.7461:7516](),[4.7516][4.18318:18358]()
    import qualified Aftok.Currency.Zcash.Zip321 as Zip321
    import Aftok.Database.PostgreSQL (QDBM)
  • edit in server/Aftok/Snaplet/Billing.hs at line 31
    [4.60244]
    [4.7531]
    import Aftok.Database.PostgreSQL (QDBM)
  • edit in server/Aftok/Snaplet/Billing.hs at line 48
    [4.16743]
    [4.16743]
    zcashBillingOps,
  • edit in server/Aftok/Snaplet/Billing.hs at line 50
    [4.16768][4.18359:18379]()
    zcashBillingOps
  • edit in server/Aftok/Snaplet/Billing.hs at line 56
    [4.7788][4.7788:7807]()
    nativeRequest,
  • edit in server/Aftok/Snaplet/Billing.hs at line 57
    [4.7822]
    [4.16830]
    nativeRequest,
  • edit in server/Aftok/Snaplet/Billing.hs at line 62
    [4.16891]
    [4.16891]
    qdbmEval,
  • edit in server/Aftok/Snaplet/Billing.hs at line 68
    [4.16978][4.18380:18393]()
    qdbmEval
  • edit in server/Aftok/Snaplet/Billing.hs at line 70
    [4.17024]
    [4.17024]
    import Aftok.Snaplet.Json (zip321PaymentRequestJSON)
  • edit in server/Aftok/Snaplet/Billing.hs at line 201
    [4.26297][4.9212:9368]()
    zip321PaymentRequestJSON :: Zip321.PaymentRequest -> Value
    zip321PaymentRequestJSON r =
    v1 . obj $
    ["zip321_request" .= (toJSON . Zip321.toURI $ r)]
  • edit in server/Aftok/Snaplet/Json.hs at line 3
    [4.27151]
    [4.27151]
    zip321PaymentRequestJSON,
  • edit in server/Aftok/Snaplet/Json.hs at line 7
    [4.27162]
    [4.27162]
    import qualified Aftok.Currency.Zcash.Zip321 as Zip321
  • replacement in server/Aftok/Snaplet/Json.hs at line 10
    [4.27228][4.27228:27260]()
    import Data.Aeson ((.=), Value)
    [4.27228]
    [4.27260]
    import Data.Aeson ((.=), Value, toJSON)
  • edit in server/Aftok/Snaplet/Json.hs at line 15
    [4.27384]
    zip321PaymentRequestJSON :: Zip321.PaymentRequest -> Value
    zip321PaymentRequestJSON r =
    v1 . obj $
    ["zip321_request" .= (toJSON . Zip321.toURI $ r)]
  • edit in server/Aftok/Snaplet/Projects.hs at line 18
    [4.24590]
    [4.52743]
    projectInviteResponseJSON,
  • edit in server/Aftok/Snaplet/Projects.hs at line 23
    [4.63481]
    [4.63481]
    import qualified Aftok.Currency.Zcash as Zcash
    import qualified Aftok.Currency.Zcash.Zip321 as Zip321
  • edit in server/Aftok/Snaplet/Projects.hs at line 31
    [4.63598]
    [4.24659]
    import Aftok.Snaplet.Json (zip321PaymentRequestJSON)
  • replacement in server/Aftok/Snaplet/Projects.hs at line 47
    [4.1468][4.1468:1519](),[4.1519][4.63786:63837](),[4.63786][4.63786:63837]()
    import Data.Aeson ((.:), (.=), Value (..), object)
    import Data.Attoparsec.ByteString (takeByteString)
    [4.1468]
    [4.24840]
    import Data.Aeson ((.:), (.:?), (.=), Value (..), object)
  • edit in server/Aftok/Snaplet/Projects.hs at line 153
    [4.1816]
    [4.8936]
    data CommsAddress
    = EmailComms Text
    | ZcashComms Text
    data ProjectInviteRequest
    = PIR
    { greetName :: Text,
    message :: Maybe Text,
    inviteBy :: CommsAddress
    }
  • replacement in server/Aftok/Snaplet/Projects.hs at line 165
    [4.8937][4.9484:9545]()
    projectInviteHandler :: ServerConfig -> S.Handler App App ()
    [4.8937]
    [4.8991]
    instance A.FromJSON ProjectInviteRequest where
    parseJSON (A.Object v) = do
    name <- v .: "greetName"
    message <- v .:? "message"
    comms <- v .: "inviteBy"
    emailComms <- fmap EmailComms <$> (comms .:? "email")
    zcashComms <- fmap ZcashComms <$> (comms .:? "zaddr")
    case emailComms <|> zcashComms of
    Nothing -> mzero
    Just addr -> pure $ PIR name message addr
    parseJSON _ = mzero
    data ProjectInviteResponse
    = ProjectInviteResponse
    { zip321URI :: Maybe Zip321.PaymentRequest
    }
    projectInviteResponseJSON :: ProjectInviteResponse -> Value
    projectInviteResponseJSON resp =
    case zip321URI resp of
    Just r -> zip321PaymentRequestJSON r
    Nothing -> object []
    projectInviteHandler :: ServerConfig -> S.Handler App App ProjectInviteResponse
  • replacement in server/Aftok/Snaplet/Projects.hs at line 192
    [4.4242][4.9070:9145](),[4.9718][4.9070:9145](),[4.53951][4.9070:9145](),[4.64472][4.9070:9145](),[4.9070][4.9070:9145]()
    toEmail <- parseParam "email" (fmap (Email . decodeUtf8) takeByteString)
    [4.64472]
    [4.64473]
    requestBody <- readRequestBody 4096
    req <- either (snapError 400 . show) pure $ A.eitherDecode requestBody
  • replacement in server/Aftok/Snaplet/Projects.hs at line 195
    [4.64504][4.4298:4321](),[4.4298][4.4298:4321](),[4.4321][4.64505:64762]()
    (Just p, invCode) <-
    snapEval $
    (,)
    <$> (runMaybeT $ findUserProject uid pid)
    <*> createInvitation pid uid toEmail t
    liftIO $
    sendProjectInviteEmail
    cfg
    (p ^. projectName)
    (Email "noreply@aftok.com")
    toEmail
    invCode
    [4.64504]
    [4.989]
    let invite email =
    snapEval $
    (,)
    <$> (runMaybeT $ findUserProject uid pid)
    <*> createInvitation pid uid email t
    case inviteBy req of
    EmailComms email -> do
    (Just p, invCode) <- invite (Email email)
    liftIO $
    sendProjectInviteEmail
    cfg
    (p ^. projectName)
    (Email "noreply@aftok.com")
    (Email email)
    invCode
    pure (ProjectInviteResponse Nothing)
    ZcashComms zaddr -> do
    (Just p, invCode) <- invite (Email "")
    pure . ProjectInviteResponse . Just
    $ Zip321.PaymentRequest . pure
    $ Zip321.PaymentItem
    { _address = Zcash.Address zaddr,
    _amount = Zcash.Zatoshi 1000,
    _memo =
    Just . Zcash.Memo . encodeUtf8 $
    "Welcome to the " <> (p ^. projectName) <> " aftok, " <> greetName req <> "\n"
    <> maybe "" (<> "\n") (message req)
    <> "https://aftok.com/app/?invcode="
    <> renderInvCode invCode
    <> "&zaddr="
    <> zaddr,
    _message = Nothing,
    _label = Nothing,
    _other = []
    }
  • replacement in server/Main.hs at line 85
    [4.77089][4.77089:77224]()
    inviteRoute = void $ method POST (projectInviteHandler cfg)
    acceptInviteRoute = void $ method POST acceptInvitationHandler
    [4.77089]
    [4.29295]
    inviteRoute =
    serveJSON (projectInviteResponseJSON) $ method POST (projectInviteHandler cfg)
    acceptInviteRoute =
    void $ method POST acceptInvitationHandler