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
  }
}
*/