Skip to content

Commit

Permalink
Add Billing module
Browse files Browse the repository at this point in the history
The module controls billing, stores messages and forms for handling
paid-only features. It's connected to the global Store, uses every
single part of it.
  • Loading branch information
viktor-yakubiv committed May 27, 2020
1 parent 6dd35cb commit 3b5c7a1
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 0 deletions.
6 changes: 6 additions & 0 deletions modules/billing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Billing module

The module controls billing, stores messages and forms for handling paid-only
features.

It's connected to the global Store, uses every single part of it.
1 change: 1 addition & 0 deletions modules/billing/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export PaymentRequiredNote from './payment-required'
108 changes: 108 additions & 0 deletions modules/billing/payment-required/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useEffect } from 'react'
import { autorun, observable } from 'mobx'
import { useObserver } from 'mobx-react-lite'

import { requestPremium as letter } from 'texts/letters'

const globalContext = observable({
user: {},
organisation: {},
})

const userInput = observable({
update(patch) {
Object.assign(this, patch)
},
})

const emailContext = observable({
toEmail: 'enterp\u0072is\u0065\u0040cor\u0065\u002e\u0061\u0063\u002euk',

// These 4 are mirrored from the global store
// with and React's effect and MobX's autorun in useSync()
organisationId: '',
organisationName: '',

get fromName() {
return userInput.name ?? globalContext.user?.name
},

get fromEmail() {
return userInput.email ?? globalContext.user?.email
},

get context() {
return {
userName: this.fromName,
userEmail: this.fromEmail,
organisationName: globalContext.organisation.name,
organisationId: globalContext.organisation.id,
}
},

get subject() {
return userInput.subject ?? letter.subject.render(this.context)
},

get body() {
return userInput.body ?? letter.body.render(this.context)
},

get mailtoUrl() {
const email = this.toEmail
const subject = encodeURIComponent(this.subject)
const body = encodeURIComponent(this.body)

return `m\u0061il\u0074\u006f\u003a${email}?subject=${subject}&body=${body}`
},

update(...args) {
userInput.update(...args)
},
})

const useSync = (store) => {
useEffect(
() =>
autorun(() => {
// Observing on root `user` and `organisation`` objects is intentional.
// It should prevent potential data overriding.
//
// However, it does not cover the case when the user changes the name
// and then makes the request. Although, if the user edited the form
// and then changed own name, it would not be confusing because
// it should be expected.
globalContext.user = store.user
globalContext.organisation = store.organisation
}),
[store]
)

useEffect(() =>
autorun(() => {
emailContext.actionUrl = store.contactUrl
emailContext.send = store.sendContactRequest.bind(store)
})
)
}

const useFormAction = () =>
useObserver(() => [emailContext.actionUrl, emailContext.send])

const useFormContext = () =>
useObserver(() => ({
subject: emailContext.subject,
body: emailContext.body,
fromName: emailContext.fromName,
fromEmail: emailContext.fromEmail,
product: 'DASHBOARD',
}))

const useNoteContext = () =>
useObserver(() => ({
email: emailContext.toEmail,
mailtoUrl: emailContext.mailtoUrl,
}))

export default emailContext
export { useFormAction, useFormContext, useNoteContext, useSync }
119 changes: 119 additions & 0 deletions modules/billing/payment-required/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useState, useCallback } from 'react'

import data, {
useFormAction,
useFormContext,
useNoteContext,
useSync,
} from './data'

import { Button } from 'design'
import Markdown from 'components/markdown'
import Form from 'components/forms/request-premium'
import { withGlobalStore } from 'store'

const getStatusText = (status) => {
if (status === 'success') return 'Sent successfully'
if (status === 'failure') return 'An error happened. Please try again'
return ''
}

const FormController = ({ onSent, onCancel, ...formProps }) => {
// editing, sending, success, failure
const [status, changeStatus] = useState('editing')

const formContext = useFormContext()
const [action, send] = useFormAction()

const handleSubmit = useCallback(
(event) => {
event.preventDefault()
const rawUserInput = new FormData(event.target)
const { body, ...metadata } = Object.fromEntries(rawUserInput.entries())
const message = {
...metadata,
message: body,
}

changeStatus('sending')
send(message).then(
(success) => {
changeStatus(success ? 'success' : 'failure')
if (typeof onSent == 'function' && success) onSent(message)
},
() => {
changeStatus('failure')
}
)
},
[send]
)

// Populate changes on blur only to prevent live editing in two places
const handleChange = useCallback((event) => {
const { name, value } = event.target
data.update({ [name]: value })
}, [])

return (
<Form
action={action}
method="post"
{...formContext}
onSubmit={handleSubmit}
onBlur={handleChange}
{...formProps}
>
<Button
variant="contained"
disabled={status !== 'editing' && status !== 'failure'}
>
Send
</Button>{' '}
{getStatusText(status)}
</Form>
)
}

const PaymentRequiredNote = ({
store,
children,
template,
variant = 'button',
}) => {
const [formShown, toggleForm] = useState(variant === 'form')
const [requestSent, toggleSent] = useState(false)
const handleToggle = useCallback(() => toggleForm(!formShown), [formShown])
const handleSend = useCallback(() => {
toggleForm(false)
toggleSent(true)
}, [])
const context = useNoteContext()

useSync(store)

return (
<>
{template && <Markdown>{template.render(context)}</Markdown>}
{children}
<p hidden={formShown} style={{ marginBottom: 0 }}>
<Button
variant="contained"
type="button"
onClick={handleToggle}
disabled={requestSent}
>
Request premium
</Button>{' '}
{requestSent && getStatusText('success')}
</p>
<FormController
hidden={!formShown}
onSent={handleSend}
onCancel={handleToggle}
/>
</>
)
}

export default withGlobalStore(PaymentRequiredNote)

0 comments on commit 3b5c7a1

Please sign in to comment.