You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OnboardingView.vue 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <template lang="pug">
  2. main.view--onboarding
  3. article(
  4. style='display: flex; flex-direction: column; align-items: center'
  5. v-if='currentStep !== survey.steps.length'
  6. )
  7. .answers(v-for='(value, key) in answered')
  8. span(v-if='key == "name" && value && currentStep == 2') Hi {{ value }}!
  9. br
  10. .step(v-for='(step, i) in survey.steps')
  11. component(
  12. :is='step.component'
  13. :question='step'
  14. :answered='answered'
  15. :responses='responses'
  16. :survey='survey'
  17. :currentStep='currentStep'
  18. :surveyStepsCount='survey.steps.length'
  19. @handle-submit='onSubmit'
  20. @update-answers='updateAnswers'
  21. v-if='step && currentStep == i'
  22. )
  23. .invalidResponseMessage(
  24. style='text-align: center'
  25. v-if='invalidResponse'
  26. )
  27. p {{ survey.steps[currentStep].invalidInputPrompt }}
  28. footer
  29. p(v-if='currentStep != 0') You have completed: {{ currentStep }} / {{ survey.steps.length }} survey steps
  30. article(v-else)
  31. SurveyCompleteView(:answers='answered' :surveySteps='survey.steps')
  32. </template>
  33. <script>
  34. import { Authenticator } from '../services/auth.service.js'
  35. import { fetchUserByEmail } from '../services/user.service.js'
  36. import {
  37. fetchProfilesByUserId,
  38. fetchProfileByProfileId,
  39. } from '../services/profile.service.js'
  40. import { surveyFactory } from '@/utils'
  41. import stepViews from '@/components/onboarding'
  42. import SurveyCompleteView from './SurveyCompleteView.vue'
  43. let sessionToken = null
  44. let accessToken = null
  45. let currentProfileId = null
  46. export default {
  47. name: 'OnboardingView',
  48. components: {
  49. ...stepViews,
  50. SurveyCompleteView,
  51. },
  52. data: () => ({
  53. answered: {},
  54. aspectQuestions: [],
  55. responses: [],
  56. currentStep: 0,
  57. survey: null,
  58. invalidResponse: false,
  59. authenticator: {},
  60. }),
  61. async created() {
  62. this.survey = await surveyFactory.createSurvey()
  63. this.authenticator = new Authenticator()
  64. // TODO: Note that all this try/catch can be in a function instead,
  65. // since it has to be done on created() and every step after 6...
  66. sessionToken = this.grabStoredCookie('siimee_session')
  67. accessToken = this.grabStoredCookie('siimee_access')
  68. // TODO: More graceful way of throwing exceptions if sessionData is not defined??
  69. try {
  70. const sessionData = await this.verifyBothTokens()
  71. await this.isEmailInRegistry(sessionData.payload.email)
  72. // TODO: Validate All routes hit by these methods using tokens in headers
  73. const userId = await this.grabUserIdByEmail(
  74. sessionData.payload.email,
  75. )
  76. currentProfileId = await this.grabProfileIdByUserId(userId)
  77. this.responses = await this.grabResponsesByProfileId(
  78. currentProfileId,
  79. )
  80. this.currentStep = this.responses.length + 3
  81. this.goToStep(this.currentStep)
  82. } catch (err) {
  83. console.error('ERROR :=>', err)
  84. this.goToStep(0)
  85. }
  86. },
  87. methods: {
  88. onSubmit() {
  89. console.log(JSON.stringify(this.answered))
  90. },
  91. async goToStep(num) {
  92. this.currentStep = num
  93. },
  94. grabStoredCookie(cookieKey) {
  95. const cookies = document.cookie
  96. .split('; ')
  97. .reduce((prev, current) => {
  98. const [name, ...value] = current.split('=')
  99. prev[name] = value.join('=')
  100. return prev
  101. }, {})
  102. const cookieVal =
  103. cookieKey in cookies ? cookies[`${cookieKey}`] : undefined
  104. return cookieVal
  105. },
  106. async verifyBothTokens() {
  107. const sessionTokenIsValid = await this.verifySessionToken(
  108. sessionToken,
  109. )
  110. const accessTokenIsValid = await this.verifyAccessToken(accessToken)
  111. if (
  112. accessTokenIsValid.status === 401 &&
  113. sessionTokenIsValid.status !== 401
  114. ) {
  115. console.warn(
  116. 'WARNING :=> Access Token Expired, but Session Token Is Still Valid, reissuing Access Token...',
  117. )
  118. // TODO: break out reissuing new tokens into separate _function
  119. const newAccessToken = await this.authenticator.getAccessToken(
  120. sessionTokenIsValid.payload,
  121. )
  122. const newAccessTokenIsValid = await this.verifyAccessToken(
  123. newAccessToken,
  124. )
  125. accessToken = newAccessToken
  126. document.cookie = `siimee_access=${newAccessToken}; max-age=600; path=/; secure`
  127. // NOTE: Resetting Session Token otherwise session
  128. // token will always expire after 10 minutes...???
  129. const newSessionToken =
  130. await this.authenticator.getSessionToken(
  131. sessionTokenIsValid.payload,
  132. )
  133. sessionToken = newSessionToken
  134. document.cookie = `siimee_session=${newSessionToken}; max-age=600; path=/; secure`
  135. return newAccessTokenIsValid
  136. } else if (
  137. accessTokenIsValid.status === 401 &&
  138. sessionTokenIsValid.status === 401
  139. ) {
  140. sessionToken = null
  141. accessToken = null
  142. currentProfileId = null
  143. throw new Error('Both Session and Access Token Are Expired!!')
  144. } else return accessTokenIsValid
  145. },
  146. async verifySessionToken(sessionToken) {
  147. if (!sessionToken) {
  148. return console.warn('WARNING :=> sessionToken is not defined')
  149. } else return await this.validateToken(sessionToken)
  150. },
  151. async verifyAccessToken(accessToken) {
  152. if (!accessToken) {
  153. return console.warn('WARNING :=> accessToken is not defined')
  154. } else return await this.validateToken(accessToken)
  155. },
  156. async validateToken(token) {
  157. const validatedToken = await this.authenticator.validateSession(
  158. token,
  159. )
  160. if (validatedToken.error) {
  161. throw new Error(validatedToken.error)
  162. } else {
  163. return validatedToken
  164. }
  165. },
  166. async isEmailInRegistry(email) {
  167. const emailIsInRegistry =
  168. await this.authenticator.checkIfEmailIsRegistered(email)
  169. if (!emailIsInRegistry) {
  170. throw new Error('Email Is NOT in Registry!')
  171. } else return emailIsInRegistry
  172. },
  173. async grabUserIdByEmail(email) {
  174. const user = await fetchUserByEmail(email)
  175. if (!user) {
  176. throw new Error('User NOT found by email')
  177. } else return user.user_id
  178. },
  179. async grabProfileIdByUserId(userId) {
  180. const profilesFromUserId = await fetchProfilesByUserId(userId)
  181. if (
  182. profilesFromUserId.length === 1 &&
  183. profilesFromUserId.status !== 401
  184. ) {
  185. return profilesFromUserId[0].profile_id
  186. } else if (profilesFromUserId.length > 1) {
  187. // TODO: Refactor once more is known on users with multiple profiles
  188. throw new Error('Multiple Profiles for this User ID')
  189. } else {
  190. throw new Error('No Profile for User ID found')
  191. }
  192. },
  193. async grabProfileByProfileId(profileId) {
  194. const profile = await fetchProfileByProfileId(profileId)
  195. if (!profile || profile.status === 401) {
  196. throw new Error(`No Profile Found for profileId ${profileId}`)
  197. } else {
  198. return profile
  199. }
  200. },
  201. async grabResponsesByProfileId(profileId) {
  202. const responses = []
  203. const profile = await this.grabProfileByProfileId(profileId)
  204. if (!profile.responses.length || profile.responses.status === 401) {
  205. throw new Error(`No Responses Found for profileId ${profileId}`)
  206. } else {
  207. profile.responses.forEach(response => {
  208. responses.push({
  209. response_key_id: response.response_key_id,
  210. val: response.val,
  211. })
  212. })
  213. return responses
  214. }
  215. },
  216. async updateAnswers(payload) {
  217. if (payload) {
  218. const k = payload.question.survey_stage
  219. this.answered[k] = payload.input
  220. // Once validated, don't log password in answered object
  221. this.answered[k] = k === 'password' ? undefined : payload.input
  222. // Hacky WorkAround for Validating Answers
  223. if (!this.survey.validateAnswer(payload)) {
  224. this.invalidResponse = true
  225. return
  226. }
  227. // Formats initial responses for response table
  228. const response = {}
  229. response.response_key_id = payload.question.response_key_id
  230. response.val = payload.input
  231. this.responses.push(response)
  232. if (k === 'aspects') return
  233. }
  234. if (currentProfileId) {
  235. // TODO: Still have to authenticate this route
  236. await surveyFactory.addNewSurveyAnswer(
  237. this.responses[this.responses.length - 1],
  238. currentProfileId,
  239. accessToken,
  240. )
  241. try {
  242. await this.verifyBothTokens(sessionToken, accessToken)
  243. } catch (err) {
  244. this.currentStep = 0
  245. this.goToStep(this.currentStep)
  246. throw new Error(err)
  247. }
  248. }
  249. if (this.currentStep > this.survey.steps.length) {
  250. this.onSubmit(this.answered)
  251. } else {
  252. this.goToStep(this.currentStep + 1)
  253. }
  254. },
  255. },
  256. }
  257. </script>
  258. <style lang="sass">
  259. .view--onboarding
  260. width: 100%
  261. max-width: 428px
  262. background-color: #fff
  263. color: #1F2024
  264. font-family: Century Gothic,CenturyGothic,AppleGothic,sans-serif
  265. margin: 0 auto
  266. article
  267. height: 100vh
  268. .answers
  269. text-align: center
  270. .w-button
  271. display: flex
  272. width: 315px
  273. height: 60px
  274. border-radius: 6
  275. background-color: #D5D5D5
  276. color: #1F2024
  277. margin: 11px auto
  278. font-weight: bold
  279. font-size: 16px
  280. text-transform: uppercase
  281. &.next-btn
  282. border-radius: 6px
  283. background-color: #5BA626
  284. color: #1F2024
  285. height: 50px
  286. width: 315px
  287. font-size: 18px
  288. padding: 7px
  289. .w-card
  290. background-color: #1F2024
  291. justify-content: center
  292. align-items: center
  293. width: 100%
  294. h3
  295. text-transform: uppercase
  296. text-align: center
  297. font-size: 28px
  298. font-weight: bold
  299. color: white
  300. margin-top: 88px
  301. p
  302. color: white
  303. font-size: 18px
  304. padding: 11px
  305. text-align: center
  306. margin: 22px auto
  307. font-weight: bold
  308. input
  309. display: flex
  310. width: 315px
  311. height: 60px
  312. padding: 11px
  313. border-radius: 6
  314. background-color: #D5D5D5
  315. color: #1F2024
  316. margin: 11px auto
  317. font-weight: bold
  318. font-size: 16px
  319. .w-select
  320. padding: 11px
  321. color: #1F2024
  322. .search-type
  323. color: #1F2024
  324. height: 50px
  325. &.question
  326. p
  327. font-size: 18px
  328. text-align: left
  329. margin: 7px auto
  330. font-weight: normal
  331. section
  332. p
  333. margin: 0
  334. font-weight: bold
  335. text-transform: capitalize
  336. .w-radio__input
  337. &.primary
  338. background-color: #FFFFFF
  339. border: #BCC5D3 1px solid
  340. .w-card__content
  341. .w-button
  342. height: 50px
  343. background-color: #5BA626
  344. </style>