| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { Form, FormControl, FormField } from '@ui/components/shadcn/ui/form'
- import { useParams } from 'common'
- import { useEffect } from 'react'
- import { useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import { FormMessage, Input } from 'ui'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { InfoTooltip } from 'ui-patterns/info-tooltip'
- import {
- MultiSelector,
- MultiSelectorContent,
- MultiSelectorList,
- MultiSelectorTrigger,
- } from 'ui-patterns/multi-select'
- import { z } from 'zod'
- import {
- ScaffoldSection,
- ScaffoldSectionContent,
- ScaffoldSectionDetail,
- } from '@/components/layouts/Scaffold'
- import { FormActions } from '@/components/ui/Forms/FormActions'
- import { FormPanel } from '@/components/ui/Forms/FormPanel'
- import { FormSection, FormSectionContent } from '@/components/ui/Forms/FormSection'
- import NoPermission from '@/components/ui/NoPermission'
- import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organization-customer-profile-query'
- import { useOrganizationUpdateMutation } from '@/data/organizations/organization-update-mutation'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- const FORM_ID = 'org-billing-email'
- const formSchema = z.object({
- billingEmail: z.string().email('Please provide a valid email address').optional(),
- additionalBillingEmails: z.string().email({ message: 'invalid_email' }).array().default([]),
- })
- const BillingEmail = () => {
- const { slug } = useParams()
- const { data: selectedOrganization } = useSelectedOrganizationQuery()
- const { name, billing_email } = selectedOrganization ?? {}
- const { can: canReadBillingEmail, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
- PermissionAction.BILLING_READ,
- 'stripe.subscriptions'
- )
- const { can: canUpdateOrganization } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'organizations'
- )
- const { data: billingCustomer, isPending: loadingBillingCustomer } =
- useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingEmail })
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema as any),
- defaultValues: {
- billingEmail: billing_email ?? '',
- additionalBillingEmails: billingCustomer?.additional_emails ?? [],
- },
- })
- const { additionalBillingEmails } = form.watch()
- const { errors } = form.formState
- const additionalEmailsError = errors.additionalBillingEmails ?? []
- const { mutate: updateOrganization, isPending: isUpdating } = useOrganizationUpdateMutation()
- const onUpdateOrganizationEmail = async (values: z.infer<typeof formSchema>) => {
- if (!canUpdateOrganization) {
- return toast.error('You do not have the required permissions to update this organization')
- }
- if (!slug) return console.error('Slug is required')
- if (!name) return console.error('Organization name is required')
- updateOrganization(
- {
- slug,
- name,
- billing_email: values.billingEmail,
- additional_billing_emails: values.additionalBillingEmails,
- },
- {
- onSuccess: () => {
- toast.success('Successfully saved settings')
- form.reset(values)
- },
- }
- )
- }
- useEffect(() => {
- if (billingCustomer) {
- form.reset({
- billingEmail: billing_email ?? '',
- additionalBillingEmails: billingCustomer.additional_emails ?? [],
- })
- }
- }, [billingCustomer])
- return (
- <ScaffoldSection>
- <ScaffoldSectionDetail>
- <div className="sticky space-y-2 top-12">
- <p className="text-foreground text-base m-0">Email Recipient</p>
- <p className="text-sm text-foreground-light m-0">
- All billing correspondence will go to this email
- </p>
- </div>
- </ScaffoldSectionDetail>
- <ScaffoldSectionContent>
- {isPermissionsLoaded && !canReadBillingEmail ? (
- <NoPermission resourceText="view this organization's email recipients" />
- ) : (
- <Form {...form}>
- <form id={FORM_ID} onSubmit={form.handleSubmit(onUpdateOrganizationEmail)}>
- <FormPanel
- footer={
- <div className="flex py-4 px-8">
- <FormActions
- form={FORM_ID}
- isSubmitting={isUpdating}
- hasChanges={form.formState.isDirty}
- handleReset={form.reset}
- disabled={!canUpdateOrganization}
- helper={
- !canUpdateOrganization
- ? 'You need additional permissions to update billing emails'
- : undefined
- }
- />
- </div>
- }
- >
- <FormSection className="px-8!">
- <FormSectionContent fullWidth loading={loadingBillingCustomer}>
- <FormField
- control={form.control}
- name="billingEmail"
- render={({ field }) => (
- <FormItemLayout label="Email address">
- <FormControl>
- <Input
- type="email"
- {...field}
- placeholder="Email"
- disabled={!canUpdateOrganization}
- />
- </FormControl>
- <FormMessage />
- </FormItemLayout>
- )}
- />
- <FormField
- control={form.control}
- name="additionalBillingEmails"
- render={({ field }) => (
- <FormItemLayout
- hideMessage
- label={
- <div className="flex items-center gap-x-1">
- <span>Additional emails</span>
- <InfoTooltip side="bottom">
- These email addresses will be CC'd in automated invoice or payment
- failure emails. Payment receipts will still only go to the primary
- billing address.
- </InfoTooltip>
- </div>
- }
- >
- <FormControl>
- <MultiSelector values={field.value} onValuesChange={field.onChange}>
- <MultiSelectorTrigger
- deletableBadge
- showIcon={false}
- mode="inline-combobox"
- label="Add additional recipients"
- badgeLimit="wrap"
- />
- <MultiSelectorContent>
- <MultiSelectorList creatable />
- </MultiSelectorContent>
- </MultiSelector>
- </FormControl>
- {/* [Joshen] Manually construct the message here as MultiSelector doesn't handle array errors from RHF atm */}
- {Array.isArray(additionalEmailsError) &&
- additionalEmailsError.length > 0 && (
- <div className="flex flex-col gap-y-1 mt-2">
- {additionalEmailsError.map((_x, idx) => (
- <p
- key={`email-error-${idx}`}
- className="text-sm text-destructive"
- >
- "{additionalBillingEmails[idx]}" is not a valid email address
- </p>
- ))}
- </div>
- )}
- </FormItemLayout>
- )}
- />
- </FormSectionContent>
- </FormSection>
- </FormPanel>
- </form>
- </Form>
- )}
- </ScaffoldSectionContent>
- </ScaffoldSection>
- )
- }
- export default BillingEmail
|