diff --git a/apps/mitt-konto/src/Components/Renew.purs b/apps/mitt-konto/src/Components/Renew.purs new file mode 100644 index 000000000..c71d11a55 --- /dev/null +++ b/apps/mitt-konto/src/Components/Renew.purs @@ -0,0 +1,83 @@ +module MittKonto.Components.Renew where + +import Prelude + +import Bottega.Models (PaymentMethod(..)) +import Data.Array (snoc) +import Data.Nullable as Nullable +import KSF.Api.Subscription as Subscription +import MittKonto.Main.UserView.Subscription.Types as Types +import Prenumerera.Package as Package +import Prenumerera.Page.SelectPeriod as SelectPeriod +import Prenumerera.Page.Payment as Payment +import React.Basic.DOM as DOM +import React.Basic.Events (handler_) +import React.Basic.Hooks (Component, useState', (/\)) +import React.Basic.Hooks as React + +data Stage = SelectPeriod | Payment Package.PackageOffer PaymentMethod | Finished + +component :: Component Types.RenewSubscription +component = do + selectPeriod <- SelectPeriod.component + payment <- Payment.component + React.component "Renew" $ \props@{package, description, user, onCancel} -> React.do + stage /\ setStage <- useState' SelectPeriod + let initialPaymentMethod = + case props.subscription.paymentMethod of + Subscription.CreditCard -> CreditCard + Subscription.UnknownPaymentMethod -> CreditCard + _ -> if props.user.address /= Nullable.null then PaperInvoice else CreditCard + -- The default action of SelectPeriod is to ask for address if + -- none is set and paper invoice is selected, but that makes + -- less sense when used in Mitt Konto. + availablePaymentMethods = + if props.user.address /= Nullable.null then [ CreditCard, PaperInvoice ] else [ CreditCard ] + closable content = DOM.div + { className: "route-wrapper payment-popup" + , children: + [ DOM.div + { className: "header-x-button" + , children: + [ DOM.div + { className: "close-button" + , children: [ DOM.div { className: "close-icon" } ] + , onClick: handler_ onCancel + } + ] + } + ] `snoc` content + } + + pure $ case stage of + SelectPeriod -> + DOM.div + { className: "route-wrapper" + , children: + [ DOM.div + { className: "close-button" + , children: [ DOM.div { className: "close-icon" } ] + , onClick: handler_ onCancel + } + ] + } <> + selectPeriod + { package + , description + , user + , initialPaymentMethod + , availablePaymentMethods + , cancel: onCancel + , next: \offer paymentMethod _ -> setStage $ Payment offer paymentMethod + } + Payment offer method -> + closable $ payment + { user + , package + , description + , offer + , method + , next: setStage Finished + } + Finished -> + closable $ DOM.text "Köpet är klart" diff --git a/apps/mitt-konto/src/Components/Subscription.purs b/apps/mitt-konto/src/Components/Subscription.purs index 82e41e074..9b698c661 100644 --- a/apps/mitt-konto/src/Components/Subscription.purs +++ b/apps/mitt-konto/src/Components/Subscription.purs @@ -78,11 +78,10 @@ render :: Types.Self -> JSX -> JSX render self@{ props: { now, subscription: sub } } informationColumn = Grid.row2 informationColumn - (if expired then mempty else Elements.subscriptionUpdates self) + (Elements.subscriptionUpdates self) { extraClasses: [ "subscription--container" ] , _data: [ Tuple "subsno" subsno ] , id: "subscription-" <> subsno } where - expired = isSubscriptionExpired sub now subsno = Subsno.toString sub.subsno diff --git a/apps/mitt-konto/src/Components/User.purs b/apps/mitt-konto/src/Components/User.purs index 69316f29d..9c539e0cb 100644 --- a/apps/mitt-konto/src/Components/User.purs +++ b/apps/mitt-konto/src/Components/User.purs @@ -9,6 +9,7 @@ import Data.Nullable as Nullable import Data.String (contains) import Data.String.Pattern (Pattern(..)) import MittKonto.Components.Mailinglists as Mailinglists +import MittKonto.Components.Renew as Renew import MittKonto.Components.Subscription (component) as Subscription import MittKonto.Main.UserView.AccountEdit as AccountEdit import MittKonto.Main.UserView.IconAction as IconAction @@ -22,7 +23,7 @@ import KSF.User.Cusno as Cusno import React.Basic (JSX) import React.Basic.DOM as DOM import React.Basic.Hooks as React -import React.Basic.Hooks (Component) +import React.Basic.Hooks (Component, useState', (/\)) import Routing.PushState (PushStateInterface) foreign import images :: { subscribe :: String } @@ -34,7 +35,9 @@ component router logger = do subscriptionComponent <- Subscription.component profile <- Profile.component mailinglists <- Mailinglists.component + renewSubscription <- Renew.component React.component "UserView" \{ state: { news, now }, setState, user } -> React.do + renewingSubscription /\ setRenewingSubscription <- useState' Nothing let profileView = Helpers.componentBlock "Mina uppgifter:" @@ -59,10 +62,11 @@ component router logger = do } subscriptionView subscription = - subscriptionComponent { subscription, user, logger, now, router } + subscriptionComponent { subscription, user, logger, now, router, renewSubscription, setRenewingSubscription } subscriptionsView = Helpers.componentBlock "Mina prenumerationer:" $ subscriptions <> [ Elements.break, subscribeImage ] where + renewing subscription = if Just subscription.subsno == renewingSubscription then " renewing" else "" subscriptions = -- Sort the canceled subscriptions to the end of the list case sortBy (comparing _.state) user.subs of @@ -75,10 +79,10 @@ component router logger = do subscriptionComponentBlockContent subscription -- If the subscription has a canceled state, we want to add extra css to it. | Subscription.isSubscriptionCanceled subscription = - Helpers.componentBlockContent " mitt-konto--canceled-subscription" $ + Helpers.componentBlockContent (" mitt-konto--canceled-subscription" <> (renewing subscription)) $ subscriptionView subscription | Subscription.isSubscriptionExpired subscription now = - Helpers.componentBlockContent " mitt-konto--expired-subscription" $ + Helpers.componentBlockContent (" mitt-konto--expired-subscription" <> (renewing subscription)) $ subscriptionView subscription | otherwise = Helpers.componentBlockContent "" $ subscriptionView subscription diff --git a/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Elements.purs b/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Elements.purs index 09fcf22cf..ae42fd737 100644 --- a/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Elements.purs +++ b/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Elements.purs @@ -10,16 +10,18 @@ import Data.Enum (enumFromTo) import Data.Foldable (foldMap, for_, null, maximum) import Data.JSDate (toDate, toDateTime) import Data.List (intercalate) +import Data.Map as Map import Data.Maybe (Maybe(..), fromMaybe, isNothing, maybe) import Data.Monoid (guard) import Data.Newtype (unwrap) +import Data.Nullable as Nullable import Data.Nullable (toMaybe) import Data.String (length, splitAt, trim) import Data.Tuple (Tuple(..)) import Effect.Aff as Aff import Effect.Class (liftEffect) import Foreign (unsafeToForeign) -import KSF.Api.Subscription (PausedSubscription, SubscriptionPaymentMethod(..), isSubscriptionPausable, isSubscriptionTemporaryAddressChangable, isPause) +import KSF.Api.Subscription (PausedSubscription, SubscriptionPaymentMethod(..), isPause, isSubscriptionExpired, isSubscriptionPausable, isSubscriptionTemporaryAddressChangable) import KSF.Api.Subscription (toString) as Subsno import KSF.AsyncWrapper as AsyncWrapper import KSF.DeliveryReclamation as DeliveryReclamation @@ -37,6 +39,8 @@ import MittKonto.Main.UserView.Subscription.Helpers as Helpers import MittKonto.Main.UserView.Subscription.Types as Types import MittKonto.Wrappers.ActionsWrapper (actionsWrapper) as ActionsWrapper import MittKonto.Wrappers.Elements (successWrapper) +import Prenumerera.Package as Prenumerera.Package +import Prenumerera.Package.Description (Description, packageDescriptions) import React.Basic (JSX) import React.Basic.DOM as DOM import React.Basic.DOM.Events (capture_, preventDefault) @@ -141,15 +145,25 @@ subscriptionEndTerm { props: { subscription: { dates: { suspend } } } } = foldMa ) $ trim <<< formatDateDots <$> (toDate =<< toMaybe suspend) subscriptionUpdates :: Types.Self -> JSX -subscriptionUpdates self@{ props: props@{ now, subscription: sub@{ subsno, package } }, state } = +subscriptionUpdates self@{ props: props@{ now, subscription: sub@{ subsno } }, state } = Grid.row_ [ actionsWrapper ] where actionsWrapper = ActionsWrapper.actionsWrapper - { actions: (if package.digitalOnly then - mempty - else - paperOnlyActions) - <> extraActions + { actions: if isSubscriptionExpired sub now + then case Tuple + <$> Map.lookup sub.package.id packageDescriptions + <*> Prenumerera.Package.fromApiPackage sub.package + of + Just (Tuple description package) + | sub.cusno == props.user.cusno && not (null sub.package.offers) && + (sub.package.digitalOnly || props.user.address /= Nullable.null) -> + [ renewUpdateIcon description package ] + _ -> mempty + else (if sub.package.digitalOnly then + mempty + else + paperOnlyActions) + <> extraActions , wrapperState: self.state.wrapperProgress , onTryAgain: self.setState _ { wrapperProgress = updateProgress } , containerClass: "subscription--actions-container flex" @@ -183,6 +197,7 @@ subscriptionUpdates self@{ props: props@{ now, subscription: sub@{ subsno, packa Just (Types.EditTemporaryAddressChange change) -> AsyncWrapper.Editing $ temporaryAddressChangeComponent self $ Just change Just Types.DeliveryReclamation -> AsyncWrapper.Editing deliveryReclamationComponent + Just (Types.RenewSubscription d p)-> AsyncWrapper.Editing $ renewSubscriptionComponent self d p Nothing -> AsyncWrapper.Ready deliveryReclamationComponent = @@ -360,6 +375,30 @@ subscriptionUpdates self@{ props: props@{ now, subscription: sub@{ subsno, packa ] } + renewUpdateIcon description package = + DOM.div + { className: "subscription--action-item" + , children: + [ DOM.div + { className: "subscription--renew-icon circle" + , onClick: showRenewSubscription + } + , DOM.span + { className: "subscription--update-action-text" + , children: + [ DOM.u_ [ DOM.text "Förnya prenumerationen" ]] + , onClick: showRenewSubscription + } + ] + } + where + showRenewSubscription = handler_ do + self.props.setRenewingSubscription $ Just subsno + self.setState _ + { updateAction = Just $ Types.RenewSubscription description package + , wrapperProgress = AsyncWrapper.Editing $ renewSubscriptionComponent self description package + } + pauseSubscriptionComponent :: Types.Self -> Maybe User.PausedSubscription -> JSX pauseSubscriptionComponent self@{ props: props@{ subscription: sub@{ package } } } editing = PauseSubscription.pauseSubscription @@ -505,3 +544,21 @@ changeButton self updateAction component = { updateAction = Just updateAction , wrapperProgress = AsyncWrapper.Editing component } + +renewSubscriptionComponent :: Types.Self -> Description -> Prenumerera.Package.Package -> JSX +renewSubscriptionComponent self@{props} description package = + props.renewSubscription + { subscription: props.subscription + , user: props.user + , description + , package + , onCancel: do + self.props.setRenewingSubscription Nothing + self.setState _ { wrapperProgress = AsyncWrapper.Ready } + , onSuccess: do + self.props.setRenewingSubscription Nothing + self.setState _ { wrapperProgress = AsyncWrapper.Ready } + , onError: const do + self.props.setRenewingSubscription Nothing + self.setState _ { wrapperProgress = AsyncWrapper.Error "Något gick fel" } + } diff --git a/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Types.purs b/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Types.purs index 1829c4e95..409964ccc 100644 --- a/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Types.purs +++ b/apps/mitt-konto/src/MittKonto/Main/Views/UserView/Subscription/Types.purs @@ -5,10 +5,13 @@ import Prelude import Data.Date (Date) import Data.Maybe (Maybe) import Effect (Effect) +import KSF.Api.Subscription (Subsno) import KSF.AsyncWrapper as AsyncWrapper import KSF.Sentry as Sentry import KSF.User as User import KSF.User (User) +import Prenumerera.Package as Prenumerera.Package +import Prenumerera.Package.Description (Description) import React.Basic (JSX) import Routing.PushState (PushStateInterface) @@ -24,6 +27,8 @@ type Props = , logger :: Sentry.Logger , now :: Date , router :: PushStateInterface + , renewSubscription :: RenewSubscription -> JSX + , setRenewingSubscription :: Maybe Subsno -> Effect Unit } type State = @@ -39,6 +44,7 @@ data SubscriptionUpdateAction | TemporaryAddressChange | EditTemporaryAddressChange User.PendingAddressChange | DeliveryReclamation + | RenewSubscription Description Prenumerera.Package.Package type Subscription = { package :: { name :: String @@ -47,3 +53,13 @@ type Subscription = , state :: String , dates :: User.SubscriptionDates } + +type RenewSubscription = + { subscription :: User.Subscription + , user :: User + , package :: Prenumerera.Package.Package + , description :: Description + , onCancel :: Effect Unit + , onSuccess :: Effect Unit + , onError :: User.UserError -> Effect Unit + } diff --git a/apps/prenumerera/spago.dhall b/apps/prenumerera/spago.dhall index 55fc3bd60..54e6b8800 100644 --- a/apps/prenumerera/spago.dhall +++ b/apps/prenumerera/spago.dhall @@ -6,13 +6,11 @@ , "arrays" , "console" , "control" - , "datetime" , "effect" , "either" , "exceptions" , "foldable-traversable" , "foreign" - , "lists" , "maybe" , "now" , "nullable" @@ -23,9 +21,7 @@ , "react-basic-hooks" , "routing" , "strings" - , "transformers" , "tuples" - , "validation" , "web-html" ] , packages = ../../packages.dhall diff --git a/apps/prenumerera/src/Prenumerera.purs b/apps/prenumerera/src/Prenumerera.purs index e8f581e40..57ca4e673 100644 --- a/apps/prenumerera/src/Prenumerera.purs +++ b/apps/prenumerera/src/Prenumerera.purs @@ -3,6 +3,7 @@ module Prenumerera where import Prelude import Bottega as Bottega +import Bottega.Models (PaymentMethod(..)) import Control.Alt ((<|>)) import Data.Array (elem, filter, mapMaybe) import Data.Either (Either(..), either, isLeft) @@ -201,6 +202,8 @@ app = do { package , description , user: u + , availablePaymentMethods: [ CreditCard, PaperInvoice ] + , initialPaymentMethod: CreditCard , next: offerAndMethodSelected , cancel: nav.pushState (unsafeToForeign {}) "/" } diff --git a/less/Subscription.less b/less/Subscription.less index f49c17bd8..8c6ed6a06 100644 --- a/less/Subscription.less +++ b/less/Subscription.less @@ -97,6 +97,10 @@ .subscription--update-icon('../images/fin-card-front-back.svg'); } +.subscription--renew-icon { + .subscription--update-icon('../images/payment-history.svg'); +} + .subscription--update-icon(@url) { display: block; width: 30px; @@ -159,3 +163,35 @@ margin-top: 0.5em; } } + + +.mitt-konto--container { + .renewing { + position: relative; + + .route-wrapper { + float: right; + } + } + + .payment-popup { + position: absolute; + margin-top: 5px; + top: 0; + left: 0; + width: 100%; + } + + .payment-terminal { + width: 100%; + height: 450px; + margin-top: 5px; + } + + .payment { + label { + display: block; + margin-bottom: 5px; + } + } +} diff --git a/apps/prenumerera/src/Package.purs b/packages/components/src/Prenumerera/Package.purs similarity index 100% rename from apps/prenumerera/src/Package.purs rename to packages/components/src/Prenumerera/Package.purs diff --git a/apps/prenumerera/src/Package/Description.purs b/packages/components/src/Prenumerera/Package/Description.purs similarity index 100% rename from apps/prenumerera/src/Package/Description.purs rename to packages/components/src/Prenumerera/Package/Description.purs diff --git a/apps/prenumerera/src/Page/Payment.purs b/packages/components/src/Prenumerera/Page/Payment.purs similarity index 100% rename from apps/prenumerera/src/Page/Payment.purs rename to packages/components/src/Prenumerera/Page/Payment.purs diff --git a/apps/prenumerera/src/Page/Register.purs b/packages/components/src/Prenumerera/Page/Register.purs similarity index 100% rename from apps/prenumerera/src/Page/Register.purs rename to packages/components/src/Prenumerera/Page/Register.purs diff --git a/apps/prenumerera/src/Page/SelectPeriod.purs b/packages/components/src/Prenumerera/Page/SelectPeriod.purs similarity index 94% rename from apps/prenumerera/src/Page/SelectPeriod.purs rename to packages/components/src/Prenumerera/Page/SelectPeriod.purs index 199a95f64..b44d1f9ba 100644 --- a/apps/prenumerera/src/Page/SelectPeriod.purs +++ b/packages/components/src/Prenumerera/Page/SelectPeriod.purs @@ -34,17 +34,19 @@ type Props = { package :: Package , description :: Description , user :: User + , initialPaymentMethod :: PaymentMethod + , availablePaymentMethods :: Array PaymentMethod , next :: PackageOffer -> PaymentMethod -> User -> Effect Unit , cancel :: Effect Unit } component :: Component Props component = do - React.component "SelectPeriod" $ \ { package, description, user, next } -> React.do + React.component "SelectPeriod" $ \ props@{ package, description, user, next, availablePaymentMethods } -> React.do let initial = _.form $ Register.initialRegisterData false $ Just user offer /\ setOffer <- useState' $ head package.offers remind /\ setRemind <- useState' false - paymentMethod /\ setPaymentMethod <- useState' CreditCard + paymentMethod /\ setPaymentMethod <- useState' props.initialPaymentMethod acceptTerms /\ setAcceptTerms <- useState' false form /\ setForm <- useState initial updateUserError /\ setUpdateUserError <- useState' false @@ -56,7 +58,7 @@ component = do Registration.formValidations form CreditCard -> next o m user let remindElement = guard remind renderRemind - paymentOfferElement = renderPaymentOffer package.offers setOffer paymentMethod setPaymentMethod + paymentOfferElement = renderPaymentOffer package.offers setOffer availablePaymentMethods paymentMethod setPaymentMethod acceptElement = renderAccept acceptTerms setAcceptTerms addressElement = guard (paymentMethod == PaperInvoice) $ renderAddress package form setForm pure $ render description remindElement paymentOfferElement addressElement acceptElement @@ -76,8 +78,8 @@ renderRemind = ] } -renderPaymentOffer :: NonEmptyArray PackageOffer -> (PackageOffer -> Effect Unit) -> PaymentMethod -> (PaymentMethod -> Effect Unit) -> JSX -renderPaymentOffer offers setOffer paymentMethod setPaymentMethod = +renderPaymentOffer :: NonEmptyArray PackageOffer -> (PackageOffer -> Effect Unit) -> Array PaymentMethod -> PaymentMethod -> (PaymentMethod -> Effect Unit) -> JSX +renderPaymentOffer offers setOffer availablePaymentMethods paymentMethod setPaymentMethod = DOM.div { className: "payment accept-terms--narrow" , children: @@ -89,7 +91,7 @@ renderPaymentOffer offers setOffer paymentMethod setPaymentMethod = , required: true , onChange: handler targetValue $ setPaymentMethod <<< decodeMethod , defaultValue: paymentOptionText paymentMethod - , children: map renderPaymentOptionOption [ CreditCard, PaperInvoice ] + , children: map renderPaymentOptionOption availablePaymentMethods } ] , DOM.label_ diff --git a/apps/prenumerera/src/Summary.purs b/packages/components/src/Prenumerera/Summary.purs similarity index 100% rename from apps/prenumerera/src/Summary.purs rename to packages/components/src/Prenumerera/Summary.purs