diff --git a/components/brave_vpn/resources/panel/api/panel_browser_api.ts b/components/brave_vpn/resources/panel/api/panel_browser_api.ts index 59d6a553b979..00a207ed2448 100644 --- a/components/brave_vpn/resources/panel/api/panel_browser_api.ts +++ b/components/brave_vpn/resources/panel/api/panel_browser_api.ts @@ -7,10 +7,12 @@ import * as BraveVPN from 'gen/brave/components/brave_vpn/brave_vpn.mojom.m.js' // Provide access to all the generated types export * from 'gen/brave/components/brave_vpn/brave_vpn.mojom.m.js' +export type SupportData = BraveVPN.ServiceHandler_GetSupportData_ResponseParams + interface API { - pageCallbackRouter: BraveVPN.PageCallbackRouter - panelHandler: BraveVPN.PanelHandlerRemote - serviceHandler: BraveVPN.ServiceHandlerRemote + pageCallbackRouter: BraveVPN.PageInterface + panelHandler: BraveVPN.PanelHandlerInterface + serviceHandler: BraveVPN.ServiceHandlerInterface } let panelBrowserAPIInstance: API @@ -35,3 +37,7 @@ export default function getPanelBrowserAPI () { } return panelBrowserAPIInstance } + +export function setPanelBrowserApiForTesting (api: API) { + panelBrowserAPIInstance = api +} diff --git a/components/brave_vpn/resources/panel/components/contact-support/index.tsx b/components/brave_vpn/resources/panel/components/contact-support/index.tsx index a67ae352b570..bacc8124dccb 100644 --- a/components/brave_vpn/resources/panel/components/contact-support/index.tsx +++ b/components/brave_vpn/resources/panel/components/contact-support/index.tsx @@ -1,54 +1,115 @@ import * as React from 'react' -import { Button } from 'brave-ui' - +import Button from '$web-components/button' +import Select from '$web-components/select' +import TextInput, { Textarea } from '$web-components/input' +import Toggle from '$web-components/toggle' import { getLocale } from '../../../../../common/locale' import * as S from './style' -import getPanelBrowserAPI from '../../api/panel_browser_api' +import getPanelBrowserAPI, * as BraveVPN from '../../api/panel_browser_api' import { CaratStrongLeftIcon } from 'brave-ui/components/icons' interface Props { - closeContactSupport: React.MouseEventHandler + onCloseContactSupport: React.MouseEventHandler } -interface ContactSupportState { +interface ContactSupportInputFields { contactEmail: string problemSubject: string problemBody: string +} + +interface ContactSupportToggleFields { shareHostname: boolean shareAppVersion: boolean shareOsVersion: boolean } +type ContactSupportState = ContactSupportInputFields & + ContactSupportToggleFields + +const defaultSupportState: ContactSupportState = { + contactEmail: '', + problemSubject: '', + problemBody: '', + shareHostname: true, + shareAppVersion: true, + shareOsVersion: true +} + +type FormElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement +type BaseType = string | number | React.FormEvent + function ContactSupport (props: Props) { - const [supportData, setSupportData] = React.useState('TODO: fill me in') + const [supportData, setSupportData] = React.useState() + const [formData, setFormData] = React.useState(defaultSupportState) + const [showErrors, setShowErrors] = React.useState(false) + // Undefined for never sent, true for is sending, false for has completed + const [isSubmitting, setIsSubmitting] = React.useState() + const [, setRemoteSubmissionError] = React.useState() - React.useEffect(async () => { - setSupportData(await getPanelBrowserAPI().getSupportData()) - }) + // Get possible values to submit + React.useEffect(() => { + getPanelBrowserAPI().serviceHandler.getSupportData() + .then(setSupportData) + }, []) - const [formData, setFormData] = React.useState({ - contactEmail: '', - problemSubject: '', - problemBody: '', - shareHostname: true, - shareAppVersion: true, - shareOsVersion: true - }) + function getOnChangeField (key: keyof ContactSupportInputFields) { + return function (e: T) { + console.log(setFormData) + const value = (typeof e === 'string' || typeof e === 'number') ? e : e.currentTarget.value + if (formData[key] === value) { + return + } + setFormData(data => ({ + ...data, + [key]: value + })) + } + } + + function getOnChangeToggle (key: keyof ContactSupportToggleFields) { + return function (isOn: boolean) { + if (formData[key] === isOn) { + return + } + setFormData(data => ({ + ...data, + [key]: isOn + })) + } + } const isValid = React.useMemo(() => { - return !!formData?.problemBody - }, [formData]) + return !supportData || + !!formData.problemBody || + !!formData.contactEmail || + !!formData.problemSubject + }, [formData, supportData]) - const handleSubmit = () => { - // Build submit data - } + const handleSubmit = async () => { + // Clear error about last submission + setRemoteSubmissionError(undefined) + // Handle submission when not valid, show user + // which fields are required + if (!isValid) { + setShowErrors(true) + return + } + // Handle is valid, submit data + setIsSubmitting(true) + const fullIssueBody = formData.problemBody + '\n' + + (formData.shareOsVersion ? `OS: ${supportData?.osVersion}\n` : '') + + (formData.shareAppVersion ? `App version: ${supportData?.appVersion}\n` : '') + + (formData.shareHostname ? `Hostname: ${supportData?.hostname}\n` : '') + + await getPanelBrowserAPI().serviceHandler.createSupportTicket( + formData.contactEmail, + formData.problemSubject, + fullIssueBody + ) - const onChangeBody = (event: React.ChangeEvent) => { - setFormData({ - ...formData, - problemBody: event.currentTarget.value - }) - console.log('changed') + // TODO: handle error case, if any? + setIsSubmitting(false) } return ( @@ -57,58 +118,99 @@ function ContactSupport (props: Props) { {getLocale('braveVpnContactSupport')} - -
  • - Your email address -
  • -
  • + + { e.preventDefault() }}> + +
  • -
  • - Describe your issue - + + + ) +} diff --git a/components/web-components/input/input.module.scss b/components/web-components/input/input.module.scss new file mode 100644 index 000000000000..72a38a2417af --- /dev/null +++ b/components/web-components/input/input.module.scss @@ -0,0 +1,45 @@ +.textInput { + max-width: 720px; + display: flex; + flex-direction: column; + gap: 5px; + font-size: 14px; + font-weight: 400; + line-height: 20px; + text-align: start; + color: var(--text2); + + > input, > textarea { + --focus-border-color: var(--highlight-color); + box-sizing: border-box; + border: none; + width: 100%; + border-radius: 4px; + outline: 1px solid var(--interactive8); + transition: outline .09s ease-in-out; + background-color: var(--background1); + padding: 10px 18px; + font-weight: 400; + font-size: 13px; + line-height: 20px; + color: var(--text1); + &:hover, &:focus-visible, &:active { + outline: 4px solid var(--focus-border); + } + &:disabled { + color: var(--disabled); + outline: 1px solid var(--disabled); + } + } + + .hasError & input { + background-color: #FFEBEB; + --focus-border-color: #FF0000; + } +} + + +.errorMessage { + margin-left: 4px; + color: #BD1531; +} \ No newline at end of file diff --git a/components/web-components/input/input.stories.tsx b/components/web-components/input/input.stories.tsx new file mode 100644 index 000000000000..a6b4f3d32255 --- /dev/null +++ b/components/web-components/input/input.stories.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react' +import Component from './index' + +export default { + title: 'Input', + component: Component, + argTypes: { + isRequired: { + type: 'boolean' + }, + isErrorAlwaysShown: { + type: 'boolean' + }, + errorMessage: { + type: 'string' + }, + disabled: { + type: 'boolean' + }, + label: { + type: 'string' + } + // scale: { + // options: ['regular', 'tiny', 'small', 'large', 'jumbo'], + // control: { type: 'select' } + // } + } +} as ComponentMeta + +const Template: ComponentStory = function (args, o) { + const [value, setValue] = React.useState('I am an input') + const handleChange: React.FormEventHandler = (e) => { + setValue(e.currentTarget.value) + } + return
    + +
    +} + +export const Everything = Template.bind({})