join-team.vue 7.8 KB

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