join-team.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <template>
  2. <div class="flex flex-col items-center justify-between min-h-screen">
  3. <div
  4. v-if="invalidLink"
  5. class="flex flex-col items-center justify-center flex-1"
  6. >
  7. <i class="pb-2 opacity-75 material-icons">error_outline</i>
  8. <h1 class="text-center heading">
  9. {{ $t("team.invalid_invite_link") }}
  10. </h1>
  11. <p class="mt-2 text-center">
  12. {{ $t("team.invalid_invite_link_description") }}
  13. </p>
  14. </div>
  15. <div
  16. v-else-if="loadingCurrentUser"
  17. class="flex flex-col items-center justify-center flex-1 p-4"
  18. >
  19. <SmartSpinner />
  20. </div>
  21. <div
  22. v-else-if="currentUser === null"
  23. class="flex flex-col items-center justify-center flex-1 p-4"
  24. >
  25. <h1 class="heading">{{ $t("team.login_to_continue") }}</h1>
  26. <p class="mt-2">{{ $t("team.login_to_continue_description") }}</p>
  27. <ButtonPrimary
  28. :label="$t('auth.login_to_hoppscotch')"
  29. class="mt-8"
  30. @click.native="showLogin = true"
  31. />
  32. </div>
  33. <div v-else class="flex flex-col items-center justify-center flex-1 p-4">
  34. <div
  35. v-if="inviteDetails.loading"
  36. class="flex flex-col items-center justify-center flex-1 p-4"
  37. >
  38. <SmartSpinner />
  39. </div>
  40. <div v-else>
  41. <div
  42. v-if="!inviteDetails.loading && E.isLeft(inviteDetails.data)"
  43. class="flex flex-col items-center p-4"
  44. >
  45. <i class="mb-4 material-icons">error_outline</i>
  46. <p>
  47. {{ getErrorMessage(inviteDetails.data.left) }}
  48. </p>
  49. <p
  50. class="flex flex-col items-center p-4 mt-8 border rounded border-dividerLight"
  51. >
  52. <span class="mb-4">
  53. {{ $t("team.logout_and_try_again") }}
  54. </span>
  55. <span class="flex">
  56. <FirebaseLogout
  57. v-if="inviteDetails.data.left.type === 'gql_error'"
  58. outline
  59. />
  60. </span>
  61. </p>
  62. </div>
  63. <div
  64. v-if="
  65. !inviteDetails.loading &&
  66. E.isRight(inviteDetails.data) &&
  67. !joinTeamSuccess
  68. "
  69. class="flex flex-col items-center justify-center flex-1 p-4"
  70. >
  71. <h1 class="heading">
  72. {{
  73. $t("team.join_team", {
  74. team: inviteDetails.data.right.teamInvitation.team.name,
  75. })
  76. }}
  77. </h1>
  78. <p class="mt-2 text-secondaryLight">
  79. {{
  80. $t("team.invited_to_team", {
  81. owner:
  82. inviteDetails.data.right.teamInvitation.creator.displayName,
  83. team: inviteDetails.data.right.teamInvitation.team.name,
  84. })
  85. }}
  86. </p>
  87. <div class="mt-8">
  88. <ButtonPrimary
  89. :label="
  90. $t('team.join_team', {
  91. team: inviteDetails.data.right.teamInvitation.team.name,
  92. })
  93. "
  94. :loading="loading"
  95. :disabled="revokedLink"
  96. @click.native="joinTeam"
  97. />
  98. </div>
  99. </div>
  100. <div
  101. v-if="
  102. !inviteDetails.loading &&
  103. E.isRight(inviteDetails.data) &&
  104. joinTeamSuccess
  105. "
  106. class="flex flex-col items-center justify-center flex-1 p-4"
  107. >
  108. <h1 class="heading">
  109. {{
  110. $t("team.joined_team", {
  111. team: inviteDetails.data.right.teamInvitation.team.name,
  112. })
  113. }}
  114. </h1>
  115. <p class="mt-2 text-secondaryLight">
  116. {{
  117. $t("team.joined_team_description", {
  118. team: inviteDetails.data.right.teamInvitation.team.name,
  119. })
  120. }}
  121. </p>
  122. <div class="mt-8">
  123. <ButtonSecondary to="/" svg="home" filled :label="$t('app.home')" />
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. <div class="p-4">
  129. <ButtonSecondary
  130. class="tracking-wide !font-bold !text-secondaryDark"
  131. label="HOPPSCOTCH"
  132. to="/"
  133. />
  134. </div>
  135. <FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
  136. </div>
  137. </template>
  138. <script lang="ts">
  139. import { computed, defineComponent, useRoute } from "@nuxtjs/composition-api"
  140. import * as E from "fp-ts/Either"
  141. import * as TE from "fp-ts/TaskEither"
  142. import { pipe } from "fp-ts/function"
  143. import { GQLError, useGQLQuery } from "~/helpers/backend/GQLClient"
  144. import {
  145. GetInviteDetailsDocument,
  146. GetInviteDetailsQuery,
  147. GetInviteDetailsQueryVariables,
  148. } from "~/helpers/backend/graphql"
  149. import { acceptTeamInvitation } from "~/helpers/backend/mutations/TeamInvitation"
  150. import { initializeFirebase } from "~/helpers/fb"
  151. import { currentUser$, onLoggedIn, probableUser$ } from "~/helpers/fb/auth"
  152. import { useReadonlyStream } from "~/helpers/utils/composables"
  153. type GetInviteDetailsError =
  154. | "team_invite/not_valid_viewer"
  155. | "team_invite/not_found"
  156. | "team_invite/no_invite_found"
  157. | "team_invite/email_do_not_match"
  158. | "team_invite/already_member"
  159. export default defineComponent({
  160. layout: "empty",
  161. setup() {
  162. const route = useRoute()
  163. const inviteDetails = useGQLQuery<
  164. GetInviteDetailsQuery,
  165. GetInviteDetailsQueryVariables,
  166. GetInviteDetailsError
  167. >({
  168. query: GetInviteDetailsDocument,
  169. variables: {
  170. inviteID: route.value.query.id as string,
  171. },
  172. defer: true,
  173. })
  174. onLoggedIn(() => {
  175. if (typeof route.value.query.id === "string") {
  176. inviteDetails.execute({
  177. inviteID: route.value.query.id,
  178. })
  179. }
  180. })
  181. const probableUser = useReadonlyStream(probableUser$, null)
  182. const currentUser = useReadonlyStream(currentUser$, null)
  183. const loadingCurrentUser = computed(() => {
  184. if (!probableUser.value) return false
  185. else if (!currentUser.value) return true
  186. else return false
  187. })
  188. return {
  189. E,
  190. inviteDetails,
  191. loadingCurrentUser,
  192. currentUser,
  193. }
  194. },
  195. data() {
  196. return {
  197. invalidLink: false,
  198. showLogin: false,
  199. loading: false,
  200. revokedLink: false,
  201. inviteID: "",
  202. joinTeamSuccess: false,
  203. }
  204. },
  205. beforeMount() {
  206. initializeFirebase()
  207. },
  208. mounted() {
  209. if (typeof this.$route.query.id === "string") {
  210. this.inviteID = this.$route.query.id
  211. }
  212. this.invalidLink = !this.inviteID
  213. // TODO: check revokeTeamInvitation
  214. // TODO: check login user already a member
  215. },
  216. methods: {
  217. joinTeam() {
  218. this.loading = true
  219. pipe(
  220. acceptTeamInvitation(this.inviteID),
  221. TE.matchW(
  222. () => {
  223. this.loading = false
  224. this.$toast.error(`${this.$t("error.something_went_wrong")}`)
  225. },
  226. () => {
  227. this.joinTeamSuccess = true
  228. this.loading = false
  229. }
  230. )
  231. )()
  232. },
  233. getErrorMessage(error: GQLError<GetInviteDetailsError>) {
  234. if (error.type === "network_error") {
  235. return this.$t("error.network_error")
  236. } else {
  237. switch (error.error) {
  238. case "team_invite/not_valid_viewer":
  239. return this.$t("team.not_valid_viewer")
  240. case "team_invite/not_found":
  241. return this.$t("team.not_found")
  242. case "team_invite/no_invite_found":
  243. return this.$t("team.no_invite_found")
  244. case "team_invite/already_member":
  245. return this.$t("team.already_member")
  246. case "team_invite/email_do_not_match":
  247. return this.$t("team.email_do_not_match")
  248. default:
  249. return this.$t("error.something_went_wrong")
  250. }
  251. }
  252. },
  253. },
  254. })
  255. </script>