-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
6dd35cb
commit 3b5c7a1
Showing
4 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export PaymentRequiredNote from './payment-required' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |