import React from 'react'; import Utils from '@tutorbook/utils'; import Button from '@tutorbook/button'; import TimeslotInput from '@tutorbook/timeslot-input'; import SubjectSelect from '@tutorbook/subject-select'; import UserDialog from '@tutorbook/user-dialog'; import firebase from '@tutorbook/firebase'; import { ApiError, User, UserJSON, Timeslot, Appt, ApptJSON, Aspect, Option, } from '@tutorbook/model'; import { signupWithGoogle } from '@tutorbook/account/signup'; import { UserContextValue, UserContext } from '@tutorbook/account'; import { TextField, TextFieldHelperText } from '@rmwc/textfield'; import axios, { AxiosResponse, AxiosError } from 'axios'; import { injectIntl, defMsg, Msg, IntlShape, IntlHelper, } from '@tutorbook/intl'; import to from 'await-to-js'; import styles from './request-dialog.module.scss'; interface RequestDialogState { time?: Timeslot; message: string; subjects: Option[]; parentName: string; parentEmail: string; submitting: boolean; submitted: boolean; err?: string; } interface RequestDialogProps { subjects: Option[]; time?: Timeslot; intl: IntlShape; user: User; className?: string; aspect: Aspect; onClosed: () => void; } /** * Google Analytics checkout steps are defined as: * 0. Opening the user dialog (logged in `src/search/lib/results.tsx`). * 1. (Optional) Selecting a subject (optional b/c subjects are pre-selected). * 2. (Optional) Selecting a timeslot (optional b/c timeslot is pre-selected). * 3. (Optional) Adding a request message. * 4. Sending the request (we only log a `purchase` event once the request has * been successfully sent by our API). */ class RequestDialog extends React.Component< RequestDialogProps, RequestDialogState > { public static readonly contextType: React.Context< UserContextValue > = UserContext; public readonly context: UserContextValue; public constructor(props: RequestDialogProps, context: UserContextValue) { super(props); this.context = context; this.state = { subjects: props.subjects, time: props.time, message: '', parentName: '', parentEmail: '', submitting: false, submitted: false, }; this.handleSubjectsChange = this.handleSubjectsChange.bind(this); this.handleTimeslotChange = this.handleTimeslotChange.bind(this); this.handleMessageChange = this.handleMessageChange.bind(this); this.handleParentNameChange = this.handleParentNameChange.bind(this); this.handleParentEmailChange = this.handleParentEmailChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } public componentDidMount(): void { firebase.analytics().logEvent('view_item', { items: this.items, }); firebase.analytics().logEvent('begin_checkout', { request: this.appt.toJSON(), items: this.items, }); } private get appt(): Appt { const { aspect, user } = this.props; const { subjects, message, time } = this.state; const { user: currentUser } = this.context; return new Appt({ time, message, attendees: [ { id: currentUser.id, roles: [aspect === 'tutoring' ? 'tutee' : 'mentee'], }, { id: user.id, roles: [aspect === 'tutoring' ? 'tutor' : 'mentor'], }, ], subjects: subjects.map((s: Option) => s.value), }); } private get items(): firebase.analytics.Item[] { const { user } = this.props; return [ { item_id: user.id, item_name: user.name, }, ]; } private handleSubjectsChange(subjects: Option[]): void { this.setState({ subjects }); firebase.analytics().logEvent('checkout_progress', { checkout_step: 1, request: this.appt.toJSON(), items: this.items, }); } private handleTimeslotChange(time: Timeslot): void { this.setState({ time }); firebase.analytics().logEvent('checkout_progress', { checkout_step: 2, request: this.appt.toJSON(), items: this.items, }); } private handleMessageChange(event: React.FormEvent): void { const message: string = event.currentTarget.value; this.setState({ message }); firebase.analytics().logEvent('checkout_progress', { checkout_step: 3, request: this.appt.toJSON(), items: this.items, }); } private handleParentNameChange( event: React.FormEvent ): void { const parentName: string = event.currentTarget.value; this.setState({ parentName }); } private handleParentEmailChange( event: React.FormEvent ): void { const parentEmail: string = event.currentTarget.value; this.setState({ parentEmail }); } private async handleSubmit(event: React.FormEvent): Promise { event.preventDefault(); firebase.analytics().logEvent('checkout_progress', { checkout_step: 4, request: this.appt.toJSON(), items: this.items, }); this.setState({ submitted: false, submitting: true }); const { parentName, parentEmail } = this.state; const { user: currentUser } = this.context; const parent: User = new User({ name: parentName, email: parentEmail }); if (!currentUser.id) { const [err] = await to( signupWithGoogle( currentUser, !currentUser.parents.length ? [parent] : undefined ) ); if (err) return this.setState({ submitted: false, submitting: false, err: `An error occurred while logging in with Google. ${err.message}`, }); } else if (!currentUser.parents.length) { const [err] = await to, AxiosError>( axios.post(`/api/users/${currentUser.id}/parents`, parent.toJSON()) ); let msg: string | null = null; if (err && err.response) { msg = err.response.data.msg; } else if (err && err.request) { msg = 'Parent creation API did not respond.'; } else if (err) { msg = err.message; } if (msg) return this.setState({ submitted: false, submitting: false, err: `An error occurred while creating your parent's profile. ${msg}`, }); } const [err, res] = await to, AxiosError>( axios.post('/api/requests', this.appt.toJSON()) ); if (err && err.response) { console.error(`[ERROR] ${err.response.data.msg}`, err.response.data); firebase.analytics().logEvent('exception', { description: `Request API responded with error: ${err.response.data.msg}`, request: this.appt.toJSON(), fatal: false, }); return this.setState({ submitted: false, submitting: false, err: `An error occurred while sending your request. ${Utils.period( err.response.data.msg || err.message )}`, }); } if (err && err.request) { console.error('[ERROR] Request API did not respond:', err.request); firebase.analytics().logEvent('exception', { description: 'Request API did not respond.', request: this.appt.toJSON(), fatal: false, }); return this.setState({ submitted: false, submitting: false, err: 'An error occurred while sending your request. Please check your Internet connection and try again.', }); } if (err) { console.error('[ERROR] Calling request API:', err); firebase.analytics().logEvent('exception', { description: `Error calling request API: ${err.message}`, request: this.appt.toJSON(), fatal: false, }); return this.setState({ submitted: false, submitting: false, err: `An error occurred while sending your request. ${Utils.period( err.message )} Please check your Internet connection and try again.`, }); } const { data: request } = res as AxiosResponse; firebase.analytics().logEvent('purchase', { request, items: this.items, transaction_id: request.id, }); return this.setState({ submitted: true, submitting: false }); } /** * Renders the `RequestDialog` that shows profile info and enables booking. */ public render(): JSX.Element { const { submitting, submitted, parentName, parentEmail, subjects, message, time, err, } = this.state; const { onClosed, user, aspect, intl } = this.props; const { user: currentUser } = this.context; const msg: IntlHelper = (m: Msg, v?: any) => intl.formatMessage(m, v); const labels: Record = defMsg({ parentName: { id: 'request-dialog.parent-name', defaultMessage: "Your parent's name", }, parentEmail: { id: 'request-dialog.parent-email', defaultMessage: "Your parent's email address", }, subjects: { id: 'request-dialog.subjects', description: 'Label for the tutoring lesson subjects field.', defaultMessage: 'Subjects', }, time: { id: 'request-dialog.time', description: 'Label for the tutoring lesson time field.', defaultMessage: 'Time', }, timeErr: { id: 'request-dialog.time-err', description: "Error message telling the user that the person they're requesting" + " isn't available during the selected times.", defaultMessage: '{name} is only available {availability}.', }, topic: { id: 'request-dialog.topic', description: 'Label for the tutoring lesson topic (previously message) field.', defaultMessage: 'Topic', }, submit: { id: 'request-dialog.submit', defaultMessage: 'Request {name}', }, signUpAndSubmit: { id: 'request-dialog.sign-up-and-submit', defaultMessage: 'Signup and request', }, }); return (
Request
{!currentUser.parents.length && ( <> )} {aspect === 'tutoring' && time && ( )}