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

Use Mitt Konto as Nets credit card update return target #1559

Merged
merged 1 commit into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions apps/mitt-konto/spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ You can edit this file as you like.
, "nullable"
, "ordered-collections"
, "prelude"
, "profunctor"
, "react-basic"
, "react-basic-dom"
, "react-basic-hooks"
Expand Down
6 changes: 6 additions & 0 deletions apps/mitt-konto/src/Components/CreditCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function openLocation(location) {
return function() {
console.log("location to "+location);
window.location = location;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
module MittKonto.Main.CreditCardUpdateView where
module MittKonto.Components.CreditCard where

import Prelude

import Bottega (BottegaError, bottegaErrorMessage)
import Bottega.Models (CreditCardRegister, CreditCardRegisterState(..), FailReason(..))
import Bottega (BottegaError(..), bottegaErrorMessage)
import Bottega.Models (CreditCardRegister, CreditCardRegisterState(..), FailReason(..), RegisterCallback(MittKonto))
import Data.Either (Either(..))
import Data.Foldable (for_)
import Data.Maybe (Maybe(..), fromMaybe, isNothing)
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Aff as Aff
import Effect.Aff.AVar (tryRead) as AVar
import Effect.Aff.AVar as Aff.AVar
import Effect.AVar (AVar)
import Effect.AVar (empty, tryPut, tryTake) as AVar
import Effect.Class (liftEffect)
Expand All @@ -19,25 +18,24 @@ import KSF.Api.Subscription (Subsno)
import KSF.AsyncWrapper as AsyncWrapper
import KSF.CreditCard.Register (render, scaRequired) as Register
import KSF.Sentry as Sentry
import KSF.User (getCreditCardRegister, registerCreditCardForSubscription) as User
import KSF.Spinner (loadingSpinner)
import KSF.User (getCreditCardRegister, registerCreditCardProcess, registerCreditCardForSubscription) as User
import KSF.User.Cusno (Cusno)
import KSF.Window (close)
import MittKonto.Routes (CreditCardCallbackParams)
import MittKonto.Wrappers (AutoClose(..), SetRouteWrapperState)
import MittKonto.Wrappers.Elements as WrapperElements
import React.Basic (JSX)
import React.Basic.Hooks (Component, component, useState, useEffectOnce, (/\))
import React.Basic.Hooks (Component, useState, useEffectOnce, (/\))
import React.Basic.Hooks as React
import React.Basic.DOM as DOM
import Web.HTML as Web.HTML
import Web.HTML.Location as Web.HTML.Location
import Web.HTML.Window as Window
import Web.HTML.Window (Window)

foreign import openLocation :: String -> Effect Unit

type BaseProps =
( cusno :: Cusno
, logger :: Sentry.Logger
, window :: Maybe Window
, subsno :: Subsno
, callback :: Maybe CreditCardCallbackParams
, cardsChanged :: Effect Unit
)

Expand Down Expand Up @@ -67,20 +65,45 @@ data UpdateState
| RegisterCreditCard
| ScaRequired String

creditCardUpdateView :: Component Props
creditCardUpdateView = do
component :: Maybe CreditCardCallbackParams -> Component Props
component registerParams = do
closed <- AVar.empty
component "CreditCardUpdateView" \props -> React.do
processResponse <- AVar.empty
-- If we have the data just start it already. It needs no auth.
case registerParams of
Nothing -> pure unit
Just register | register.responseCode == "OK" -> Aff.launchAff_ do
flip Aff.AVar.put processResponse =<< User.registerCreditCardProcess register.transactionId
Just _ -> do
_ <- flip AVar.tryPut processResponse $
Left $ BottegaUnexpectedError "Nets response code was not OK"
pure unit

React.component "CreditCardUpdateView" \props -> React.do
state /\ setState <- useState initialState

let self = { state, setState, props}
useEffectOnce do
props.setWrapperState \s -> s { titleText = "Uppdatera ditt kredit- eller bankkort" }
_ <- AVar.tryTake closed
Aff.launchAff_ $ registerCreditCard self closed
pure do
_ <- AVar.tryPut unit closed
pure unit
-- Just in case user navigates again after initial load, get it
-- from props.
case props.callback of
Nothing -> do
props.setWrapperState \s -> s { titleText = "Uppdatera ditt kredit- eller bankkort" }
setState _ { asyncWrapperState = AsyncWrapper.Editing $ DOM.text
"Du styrs strax till vår betalningsbehandlare Nets."
}
Aff.launchAff_ $ registerCreditCard self
pure mempty
Just callbackParams -> do
props.setWrapperState \s -> s { titleText = "Uppdatera ditt kredit- eller bankkort" }
setState _ { asyncWrapperState = AsyncWrapper.Editing $ DOM.text "Vänligen vänta. Behandlas."
}

_ <- AVar.tryTake closed
Aff.launchAff_ $ callbackDone self closed processResponse callbackParams
pure do
_ <- AVar.tryPut unit closed
pure unit
pure $ render self

initialState :: State
Expand Down Expand Up @@ -109,52 +132,56 @@ render { setState, state: { asyncWrapperState, updateState } } =
AsyncWrapper.asyncWrapper
{ wrapperState: asyncWrapperState
, readyView: content
, editingView: identity
, editingView: (_ <> loadingSpinner)
, successView: fromMaybe mempty
, errorView: \err -> WrapperElements.errorWrapper onTryAgain err
, loadingView: identity
}
onTryAgain :: Effect Unit
onTryAgain = setState \s -> s { asyncWrapperState = AsyncWrapper.Ready }

registerCreditCard :: Self -> AVar Unit -> Aff Unit
registerCreditCard self@{ setState, props: { logger, setWrapperState, subsno, window }, state } closed = do
globalWindow <- liftEffect Web.HTML.window
creditCardRegister <- User.registerCreditCardForSubscription subsno
-- Take user to Nets terminal
registerCreditCard :: Self -> Aff Unit
registerCreditCard self@{ props: { logger, subsno } } = do
creditCardRegister <- User.registerCreditCardForSubscription (Just MittKonto) subsno
case creditCardRegister of
Right register@{ terminalUrl: Just url } -> do
let newState = state { updateState = RegisterCreditCard, paymentTerminal = Just url.paymentTerminalUrl }
liftEffect do
case window of
Just w -> do
l <- Window.location w
Web.HTML.Location.setHref url.paymentTerminalUrl l
Nothing -> do
void $ Window.open url.paymentTerminalUrl "_blank" "noopener" globalWindow
setState \_ -> newState
setWrapperState _ { closeable = true }
void $ Aff.forkAff $ startRegisterPoller self { state = newState } closed register
Right { terminalUrl: Just { paymentTerminalUrl: url } } -> do
liftEffect $ openLocation url
Right { terminalUrl: Nothing } -> do
liftEffect $ for_ window close
liftEffect do
logger.log "No terminal url received" Sentry.Error
onError self "2"
Left err -> do
liftEffect $ for_ window close
liftEffect do
let msg = bottegaErrorMessage err
-- Sentry is flooded, may or may not show up
logger.log ("Got the following error when registering credit card: " <> msg) Sentry.Error
onError self $ "3, " <> msg

-- Tell Bottega we're done with Nets terminal and poll for a result
callbackDone :: Self -> AVar Unit -> AVar (Either BottegaError Unit) -> CreditCardCallbackParams -> Aff Unit
callbackDone self@{ state, props: { setWrapperState } } closed processResult register = do
liftEffect $ setWrapperState _ { closeable = true }
let newState = state { updateState = RegisterCreditCard }
process <- Aff.AVar.read processResult
case process of
Right _ -> startRegisterPoller (self { state = newState }) closed register
Left err -> liftEffect $ onError self $ "1, " <> bottegaErrorMessage err

killRegisterPoller :: State -> Aff Unit
killRegisterPoller state = Aff.killFiber (error "Canceled poller") state.poller

startRegisterPoller :: Self -> AVar Unit -> CreditCardRegister -> Aff Unit
startRegisterPoller self@{ setState, state } closed creditCardRegister = do
startRegisterPoller :: Self -> AVar Unit -> CreditCardCallbackParams -> Aff Unit
startRegisterPoller self@{ setState, state } closed {registerNumber, registerCardId} = do
newPoller <- Aff.forkAff do
killRegisterPoller state
newPoller <- Aff.forkAff $ pollRegister self closed (Right creditCardRegister)
register <- User.getCreditCardRegister registerCardId registerNumber
let delay = case (_.state <<< _.status) <$> register of
Right CreditCardRegisterStarted -> true
Right CreditCardRegisterCreated -> true
_ -> false
when delay $ Aff.delay $ Aff.Milliseconds 1000.0
newPoller <- Aff.forkAff $ pollRegister self closed register
Aff.joinFiber newPoller
liftEffect $ setState _ { poller = newPoller }

Expand Down Expand Up @@ -183,7 +210,7 @@ pollRegister self@{ props: { logger } } closed (Right register) = do
where
delayedPollRegister :: Either BottegaError CreditCardRegister -> Aff Unit
delayedPollRegister eitherRegister = do
componentOpen <- isNothing <$> AVar.tryRead closed
componentOpen <- isNothing <$> Aff.AVar.tryRead closed
when componentOpen do
Aff.delay $ Aff.Milliseconds 1000.0
pollRegister self closed eitherRegister
Expand All @@ -204,8 +231,8 @@ onSuccess { setState, props: { setWrapperState, cardsChanged } } = do
setState _ { asyncWrapperState = AsyncWrapper.Success $ Just $ WrapperElements.successWrapper Nothing "Betalningsinformationen har uppdaterats. Du styrs strax tillbaka till kontosidan." }
setWrapperState _ { closeable = true
, closeAutomatically = Delayed 5000.0
, onClose = cardsChanged
}
cardsChanged

onError :: Self -> String -> Effect Unit
onError { setState } errMsg = setState _ { asyncWrapperState = AsyncWrapper.Error $ "Något gick fel. Vänligen försök pånytt, eller ta kontakt med vår kundtjänst. Felkod: " <> show errMsg }
33 changes: 20 additions & 13 deletions apps/mitt-konto/src/MittKonto/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ import KSF.Tracking as Tracking
import KSF.User as User
import KSF.User.Login as Login
import Foreign (unsafeToForeign)
import MittKonto.Components.CreditCard as Components.CreditCard
import MittKonto.Components.Paywall as Components.Paywall
import MittKonto.Components.User as Components.User
import MittKonto.Main.CreditCardUpdateView (creditCardUpdateView) as CreditCardUpdateView
import MittKonto.Main.Elements as Elements
import MittKonto.Main.Helpers as Helpers
import MittKonto.Main.Types as Types
import MittKonto.Main.Views (alertView, footerView, loginView, navbarWrapper) as Views
import MittKonto.Main.Views (alertView, footerView, loginView, creditCardCallbackView, navbarWrapper) as Views
import MittKonto.Payment.PaymentAccordion as PaymentAccordion
import MittKonto.Payment.PaymentDetail as PaymentDetail
import MittKonto.Payment.Types as Payments
import MittKonto.Routes (MittKontoRoute(..), needsLogin, routes)
import MittKonto.Routes (MittKontoRoute(..), needsLogin, routes, creditCardCallbackParams)
import MittKonto.Search as Search
import MittKonto.Wrappers as Wrappers
import React.Basic (JSX)
Expand All @@ -62,7 +62,8 @@ app = do
search <- Search.search
payments <- Wrappers.routeWrapper router PaymentAccordion.paymentAccordion
paymentDetail <- Wrappers.routeWrapper router PaymentDetail.paymentDetail
creditCardUpdate <- Wrappers.routeWrapper router CreditCardUpdateView.creditCardUpdateView
creditCardUpdate <- Wrappers.routeWrapper router $ Components.CreditCard.component $
creditCardCallbackParams initialRoute
now <- Now.nowDate
loginComponent <- Login.login
timeout <- Timeout.newTimer
Expand All @@ -88,6 +89,7 @@ app = do
passwordReset <- Reset.resetPassword
component "MittKonto" \_ -> React.do
state /\ setState <- useState initialState
loggingIn /\ setLoggingIn <- useState' true
_ <- News.useNews $ \n -> setState _ { news = News.render n }
isPersonating /\ setPersonating <- useState' false
route /\ setRoute <- useState' initialRoute
Expand Down Expand Up @@ -123,6 +125,7 @@ app = do
admin <- User.isAdminUser
setState $ (Types.setActiveUser $ Just user) <<< (_ { adminMode = admin } )
logger.setUser $ Just user
setLoggingIn false
Aff.launchAff_ $ Timeout.startTimer duration timeout do
liftEffect logout

Expand All @@ -138,8 +141,8 @@ app = do
Tracking.login (Just user.cusno) "magic login" "success"
setUser validScope user
Nothing -> do
pure unit
Nothing -> pure unit
setLoggingIn false
Nothing -> setLoggingIn false
Aff.runAff_ (setState <<< Types.setAlert <<< either Helpers.errorAlert (const Nothing))
$ Spinner.withSpinner (setState <<< Types.setLoading) attemptMagicLogin
matchesWith routeParse (const setRoute) router
Expand Down Expand Up @@ -190,30 +193,30 @@ app = do
, route: "/fakturor/:invno"
, routeFrom: "/fakturor"
}
creditCardUpdateInputs window subsno user =
creditCardUpdateInputs subsno callback user =
{ subsno
, callback
, cusno: user.cusno
, logger: logger
, window: window
, cardsChanged: setCardsChanged \s -> s + 1
}
creditCardUpdateView subsno user = case state.creditCards of
creditCardUpdateView subsno callback user = case state.creditCards of
Nothing -> Spinner.loadingSpinner
Just _ -> fromMaybe mempty do
let subs = find ((_ == subsno) <<< _.subsno) user.subs
w <- state.window
guard ((_.paymentMethod <$> subs) == Just CreditCard) $ pure $
creditCardUpdate
{ contentProps: creditCardUpdateInputs w subsno user
{ contentProps: creditCardUpdateInputs subsno callback user
, closeType: Wrappers.XButton
, route: "/betalkort/uppdatera"
, routeFrom: "/"
}
navToMain = router.pushState (unsafeToForeign {}) "/"
passwordResetView code = passwordReset { user: state.activeUser
, code
, passwordChangeDone
, setPasswordChangeDone
, navToMain: router.pushState (unsafeToForeign {}) "/"
, navToMain
}
userView user =
userComponent
Expand All @@ -232,7 +235,11 @@ app = do
PasswordRecovery3 -> passwordResetView Nothing
PasswordRecoveryCode code -> passwordResetView $ Just code
PasswordRecoveryCode2 code -> passwordResetView $ Just code
CreditCardUpdate subsno -> foldMap (creditCardUpdateView subsno) state.activeUser
CreditCardUpdate subsno -> foldMap (creditCardUpdateView subsno Nothing) state.activeUser
CreditCardCallback subsno response ->
maybe
(Views.creditCardCallbackView loggingIn navToMain)
(creditCardUpdateView subsno (Just response)) state.activeUser
Paywall -> paywallView
content = if isNothing state.activeUser && needsLogin route
then Views.loginView { state, setState } (setUser (Nothing :: Maybe Days)) logger
Expand Down
26 changes: 25 additions & 1 deletion apps/mitt-konto/src/MittKonto/Main/Views.purs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module MittKonto.Main.Views
( module Views
, alertView
, creditCardCallbackView
, footerView
, navbarWrapper
)
Expand All @@ -21,7 +22,6 @@ import KSF.Footer as Footer
import KSF.Navbar.Component as Navbar
import KSF.User.Cusno as Cusno
import MittKonto.Main.Helpers as Helpers
import MittKonto.Main.CreditCardUpdateView (creditCardUpdateView) as Views
import MittKonto.Main.LoginView (loginView) as Views
import MittKonto.Main.Types as Types
import React.Basic (JSX)
Expand Down Expand Up @@ -89,3 +89,27 @@ alertView alert =

footerView :: JSX
footerView = Footer.render

creditCardCallbackView :: Boolean -> Effect Unit -> JSX
creditCardCallbackView true navToMain =
DOM.div_
[ DOM.text "Vänligen vänta"
, DOM.div_
[ DOM.a
{ href: "/"
, onClick: handler preventDefault $ const navToMain
, children: [ DOM.text "Eller avbryt" ]
}
]
]
creditCardCallbackView false navToMain =
DOM.div_
[ DOM.text "Återställning av användarsession misslyckades"
, DOM.div_
[ DOM.a
{ href: "/"
, onClick: handler preventDefault $ const navToMain
, children: [ DOM.text "Avbryt" ]
}
]
]
Loading
Loading