Integration Documentation
Frontend
packages/frontend/src/App.tsx
import 'normalize.css'
import '@blueprintjs/core/lib/css/blueprint.css'
import '@blueprintjs/icons/lib/css/blueprint-icons.css'
import React from 'react'
import styled, { createGlobalStyle } from 'styled-components'
import { FocusStyleManager, H4 } from '@blueprintjs/core'
import ChallengePanel from './ChallengePanel'
import stories from './stories'
FocusStyleManager.onlyShowFocusOnTabs()
const GlobalStyles = createGlobalStyle`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f9f9fa;
}
`
const Wrap = styled.div`
width: 800px;
margin: 15px auto;
`
const Story = styled.div`
display: flex;
flex-direction: row;
margin: 15px 0;
`
const StoryInfo = styled.div`
flex: 1;
padding: 15px;
`
const App: React.FC = () => (
<Wrap>
<GlobalStyles />
{stories.map(({ name, state }, idx) => (
<Story key={idx}>
<StoryInfo>
<H4>{name}</H4>
</StoryInfo>
<ChallengePanel state={state} />
</Story>
))}
</Wrap>
)
export default App
packages/frontend/src/ChallengePanel.tsx
workflow: Workflow | null
}
interface ChallengePanelProps {
state?: ChallengePanelState
}
export const initialState: ChallengePanelState = {
loading: true,
assessmentUrl: null,
showingAssessment: false,
user: null,
workflow: null
}
const ChallengePanel: React.FC<ChallengePanelProps> = ({ state = initialState }) => {
const [loading, setLoading] = useState(state.loading)
const [assessmentUrl, setAssessmentUrl] = useState(state.assessmentUrl)
const [showingAssessment, setShowingAssessment] = useState(state.showingAssessment)
const [user, setUser] = useState(state.user)
const [, setWorkflow] = useState(state.workflow)
const { challenge, challengeSubscription } = user || {}
const loadUserAndLatestWorkflow = async (): Promise<void> => {
setLoading(true)
const userResponse = await getCurrentUser()
if (userResponse.success) {
setUser(userResponse.user)
if (userResponse.user && userResponse.user.dietIdWorkflowId) {
const workflowResponse = await getWorkflow(userResponse.user.dietIdWorkflowId)
if (workflowResponse.success) {
setWorkflow(workflowResponse.workflow)
}
}
}
setLoading(false)
}
const closeAssessment = async (): Promise<void> => {
setShowingAssessment(false)
}
// load the Diet ID user and latest workflow when the panel is mounted
useEffect(() => {
//loadUserAndLatestWorkflow()
}, [])
// close or reload the Diet ID user and latest workflow when the user completes an assessment
useEffect(() => {
const onReceiveMessage = ({ origin, data }: MessageEvent) => {
if (origin.includes('.dietid.com') && data === 'dietid:assessmentComplete') {
// You can monitor incoming messages and choose when to close, reload or any other action
loadUserAndLatestWorkflow() || closeAssessment()
}
}
// enable cross-frame communication with Diet ID
window.addEventListener('message', onReceiveMessage, false)
return () => {
window.removeEventListener('message', onReceiveMessage)
}
}, [])
const onStartWorkflowButtonClick = async (): Promise<void> => {
setLoading(true)
const ssoResponse = await createSSO({
partnerUserId: '42' as PartnerUserID,
email: 'demo@example.com',
firstName: 'John',
lastName: 'Doe'
})
if (ssoResponse.success) {
setAssessmentUrl(ssoResponse.url)
setShowingAssessment(true)
}
setLoading(false)
}
const onStartChallengeButtonClick = async (startTomorrow = false): Promise<void> => {
if (challenge) {
setLoading(true)
await createChallengeSubscription(challenge.id, startTomorrow)
await loadUserAndLatestWorkflow()
setLoading(false)
}
}
const onCheckinButtonClick = async ({
successful,
forYesterday
}: {
successful: boolean
forYesterday: boolean
}): Promise<void> => {
setLoading(true)
console.log({ successful, forYesterday })
//await createCheckin(successful, forYesterday)
setLoading(false)
}
// the user or workflow data hasn't been loaded yet, so we show a loading screen
if (loading) {
return (
<Wrap>
<Spinner size={Spinner.SIZE_LARGE} />
</Wrap>
)
}
// the user clicked on the "Start Assessment" button, so embed Diet ID
if (showingAssessment) {
// Diet ID will call window.parent.postMessage('dietid:assessmentComplete', '*') when the assessment is complete
// this invokes the above "onReceiveMessage" callback.
return (
<WorkflowWrap>
<iframe src={assessmentUrl || ''} title="Diet Assessment" frameBorder="0" />
</WorkflowWrap>
)
}
// we've loaded the data from the backend but a Diet ID counterpart doesn't exist or hasn't completed the assessment,
// so invite user to complete a Diet ID assessment
if (!user || !user.dietIdWorkflowId || !user.dietIdealWorkflowId) {
return (
<Wrap>
<Message>
<Icon icon="circle-arrow-right" iconSize={50} intent={Intent.PRIMARY} />
<H3>Let's get started with your dietary assessment!</H3>
</Message>
<Toolbar>
<Button text="Start Assessment" onClick={onStartWorkflowButtonClick} fill />
</Toolbar>
</Wrap>
)
}
// the user has no possible challenges, so we tell them
if (!challenge) {
return (
<Wrap>
<Message>
<Icon icon="notifications" iconSize={50} intent={Intent.PRIMARY} />
<H3>No challenges available</H3>
</Message>
</Wrap>
)
}
// the user has a possible challenge but hasn't started, so we invite them to start today or tomorrow
if (!challengeSubscription) {
return (
<Wrap>
<Message>
<Icon icon="circle-arrow-right" iconSize={50} intent={Intent.PRIMARY} />
<H3>Your challenge: {challenge.name}. Would you like to start?</H3>
</Message>
<Toolbar>
<Button text="Start Today" onClick={() => onStartChallengeButtonClick(false)} fill />
<Button text="Start Tomorrow" onClick={() => onStartChallengeButtonClick(true)} fill />
</Toolbar>
</Wrap>
)
}
// StartingTomorrow: the user started a challenge but opted to start tomorrow, so we wait
// MissedYesterday: the user is in a challenge and missed checkin yesterday, so we give them a chance to check in for yesterday
// Active: the user is in a challenge and has not yet checked in today, so we ask them to check in
// Succeeded: the user completed a challenge, so we congratulate them
// Failed: the user failed a challenge, so we ask if they'd like to complete it again
return (
<Wrap>
{challengeSubscription.state === 'StartingTomorrow' && (
<Message>
<Icon icon="time" iconSize={50} intent={Intent.PRIMARY} />
<H3>Your challenge starts tomorrow!</H3>
</Message>
)}
{challengeSubscription.state === 'MissedYesterday' && (
<>
<Message>
<Icon icon="help" iconSize={50} intent={Intent.WARNING} />
<H3>You missed check-in yesterday. Did you complete your challenge?</H3>
</Message>
<Toolbar>
<Button
text="Yes"
icon="thumbs-up"
onClick={() => onCheckinButtonClick({ successful: true, forYesterday: true })}
fill
/>
<Button
text="No"
icon="thumbs-down"
onClick={() => onCheckinButtonClick({ successful: false, forYesterday: true })}
fill
/>
</Toolbar>
</>
)}
{challengeSubscription.state === 'Active' && (
<>
<Message>
<Icon icon="tick" iconSize={50} intent={Intent.PRIMARY} />
<H3>Did you complete your challenge?</H3>
</Message>
<Toolbar>
<Button
text="Yes"
icon="thumbs-up"
onClick={() => onCheckinButtonClick({ successful: true, forYesterday: false })}
fill
/>
<Button
text="No"
icon="thumbs-down"
onClick={() => onCheckinButtonClick({ successful: false, forYesterday: false })}
fill
/>
</Toolbar>
</>
)}
{challengeSubscription.state === 'Succeeded' && (
<Message>
<Icon icon="tick-circle" iconSize={50} intent={Intent.SUCCESS} />
<H3>Yay! You succeeded in your challenge.</H3>
</Message>
)}
{challengeSubscription.state === 'Failed' && (
<Message>
<Icon icon="heart-broken" iconSize={50} intent={Intent.DANGER} />
<H3>You failed your challenge. Would you like to try again?</H3>
</Message>
)}
</Wrap>
)
return null
}
export default ChallengePanel
packages/frontend/src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
packages/frontend/src/react-app-env.d.ts
/// <reference types="react-scripts" />
packages/frontend/src/stories.tsx
import { fixtures, ChallengeSubscription } from 'dietid'
import { initialState, ChallengePanelState } from './ChallengePanel'
/*
loading: true,
assessmentUrl: null,
showingAssessment: false,
user: null,
workflow: null
*/
const withChallengeSubscriptionState = (state: ChallengeSubscription['state']) => ({
...initialState,
loading: false,
user: {
...fixtures.user,
challengeSubscription: { ...fixtures.user.challengeSubscription, state }
},
workflow: fixtures.workflowWithPHI
})
const stories: { name: string; state: ChallengePanelState }[] = [
{
name: 'Loading user and latest id and ideal workflows',
state: initialState
},
{
name: 'Loaded but no user',
state: { ...initialState, loading: false }
},
{
name: 'Loaded user but no id workflow completed',
state: { ...initialState, loading: false, user: { ...fixtures.user, dietIdWorkflowId: null } }
},
{
name: 'Loaded user but no ideal workflow completed',
state: { ...initialState, loading: false, user: { ...fixtures.user, dietIdealWorkflowId: null } }
},
{
name: 'Loaded user and showing workflow',
state: {
...initialState,
loading: false,
user: fixtures.user,
assessmentUrl:
'https://426cf0ecdc.staging.dietid.com/#/u/LkseVM5y1co9yx%2FZrMRU1A%3D%3D%24HTtbpJ4MsZaFgzBExIE7YfprkKC54x%2Fjhqa4OvZ7ZQqI8sTvgAoj9O%2FYKbNM%2FdDUH28as46vE75PYN4PlCYfhxhAfMZeM8fPnO1TFRZhrF0%3D',
showingAssessment: true
}
},
{
name: 'Loaded user and workflow but no available challenge',
state: {
...initialState,
loading: false,
user: { ...fixtures.user, challenge: null },
workflow: fixtures.workflowWithPHI
}
},
{
name: 'Loaded user, workflow, and challenge but no challenge subscription',
state: {
...initialState,
loading: false,
user: { ...fixtures.user, challengeSubscription: null },
workflow: fixtures.workflowWithPHI
}
},
{
name: 'Loaded user, workflow, challenge, and challenge subscription starting tomorrow',
state: withChallengeSubscriptionState('StartingTomorrow')
},
{
name: 'Loaded user, workflow, challenge, and challenge subscription missed yesterday',
state: withChallengeSubscriptionState('MissedYesterday')
},
{
name: 'Loaded user, workflow, challenge, and challenge subscription with no checkin',
state: withChallengeSubscriptionState('Active')
},
{
name: 'Loaded user, workflow, challenge, and challenge subscription with successful checkin',
state: withChallengeSubscriptionState('Succeeded')
},
{
name: 'Loaded user, workflow, challenge, and challenge subscription with failed checkin',
state: withChallengeSubscriptionState('Failed')
}
]
export default stories
packages/frontend/package.json
{
"name": "@dietid/frontend",
"version": "0.1.0",
"description": "Example frontend integrated with Diet ID",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"lint": "./node_modules/.bin/eslint src/*",
"watch": "npm run start"
},
"dependencies": {
"@blueprintjs/core": "^3.24.0",
"@blueprintjs/icons": "^3.14.0",
"dietid": "^0.1.0",
"normalize.css": "^8.0.1",
"styled-components": "^5.0.1"
},
"devDependencies": {
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/styled-components": "^5.0.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.0",
"typescript": "~3.7.2"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
packages/frontend/tsconfig.json
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}
/*
in tsconfig.base but not in cra
{
"compilerOptions": {
"declaration": true,
"experimentalDecorators": true,
"importHelpers": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"pretty": true,
"removeComments": false,
"sourceMap": true,
"stripInternal": true
}
}
*/