| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- import { Elements } from '@stripe/react-stripe-js'
- import { loadStripe, StripeAddressElement, StripeElementsOptions } from '@stripe/stripe-js'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useQueryClient } from '@tanstack/react-query'
- import { useParams } from 'common'
- import { useTheme } from 'next-themes'
- import { useEffect, useMemo, useRef, useState } from 'react'
- import { toast } from 'sonner'
- import { Button, Card, CardFooter, Form } from 'ui'
- import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
- import { BillingCustomerDataForm } from './BillingCustomerDataForm'
- import { useBillingCustomerDataForm } from './useBillingCustomerDataForm'
- import {
- getAddressElementAppearanceOptions,
- STRIPE_ELEMENT_FONTS,
- } from '@/components/interfaces/Billing/Payment/Payment.utils'
- import {
- ScaffoldSection,
- ScaffoldSectionContent,
- ScaffoldSectionDetail,
- } from '@/components/layouts/Scaffold'
- import AlertError from '@/components/ui/AlertError'
- import NoPermission from '@/components/ui/NoPermission'
- import PartnerManagedResource from '@/components/ui/PartnerManagedResource'
- import { organizationKeys } from '@/data/organizations/keys'
- import { isPartnerBillingOrganization } from '@/data/organizations/managed-by-utils'
- import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organization-customer-profile-query'
- import { useOrganizationCustomerProfileUpdateMutation } from '@/data/organizations/organization-customer-profile-update-mutation'
- import { useOrganizationTaxIdQuery } from '@/data/organizations/organization-tax-id-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { STRIPE_PUBLIC_KEY } from '@/lib/constants'
- const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
- export const BillingCustomerData = () => {
- const { slug } = useParams()
- const queryClient = useQueryClient()
- const { resolvedTheme } = useTheme()
- const { data: selectedOrganization } = useSelectedOrganizationQuery()
- const { can: canReadBillingCustomerData, isSuccess: isPermissionsLoaded } =
- useAsyncCheckPermissions(PermissionAction.BILLING_READ, 'stripe.customer')
- const { can: canUpdateBillingCustomerData } = useAsyncCheckPermissions(
- PermissionAction.BILLING_WRITE,
- 'stripe.customer'
- )
- const {
- data: customerProfile,
- error,
- isPending: isLoading,
- isSuccess,
- } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingCustomerData })
- const {
- data: taxId,
- error: errorLoadingTaxId,
- isPending: isLoadingTaxId,
- isSuccess: loadedTaxId,
- } = useOrganizationTaxIdQuery({ slug })
- const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation({
- onError: () => {},
- })
- const [isSubmitting, setIsSubmitting] = useState(false)
- const addressElementRef = useRef<StripeAddressElement | null>(null)
- const {
- form,
- handleSubmit,
- handleReset,
- isDirty,
- resetKey,
- onAddressChange,
- applyAddressElementValue,
- markCurrentValuesAsSaved,
- addressCountry,
- addressOptions,
- } = useBillingCustomerDataForm({
- customerProfile,
- taxId,
- onCustomerDataChange: async (data) => {
- setIsSubmitting(true)
- try {
- await updateCustomerProfile({
- slug,
- address: data.address,
- billing_name: data.billing_name,
- tax_id: data.tax_id,
- })
- toast.success('Successfully updated billing data')
- queryClient.setQueriesData<any[]>(
- { queryKey: organizationKeys.list(), exact: true },
- (prev) => {
- if (!prev) return prev
- return prev.map((org) =>
- org.slug === slug
- ? {
- ...org,
- ...(data.address !== undefined ? { organization_missing_address: false } : {}),
- ...(data.tax_id !== undefined
- ? { organization_missing_tax_id: data.tax_id == null }
- : {}),
- }
- : org
- )
- }
- )
- } catch (error) {
- toast.error(
- `Failed updating billing data: ${error instanceof Error ? error.message : 'Unknown error'}`
- )
- throw error
- } finally {
- setIsSubmitting(false)
- }
- },
- })
- useEffect(() => {
- addressElementRef.current = null
- }, [resetKey])
- const onFormSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- try {
- if (addressElementRef.current) {
- const addressResult = await addressElementRef.current.getValue()
- applyAddressElementValue(addressResult)
- }
- const result = await handleSubmit()
- if (result.status === 'error') {
- toast.error(result.message)
- return
- }
- markCurrentValuesAsSaved(
- result.submittedState.addressValue,
- result.submittedState.taxIdValues
- )
- } catch {
- // Save failure toasts are handled inside onCustomerDataChange.
- }
- }
- const isSubmitDisabled = !isDirty || !canUpdateBillingCustomerData || isSubmitting
- const stripeElementsOptions: StripeElementsOptions = useMemo(
- () =>
- ({
- mode: 'setup',
- currency: 'usd',
- appearance: getAddressElementAppearanceOptions(resolvedTheme),
- fonts: STRIPE_ELEMENT_FONTS,
- }) as any,
- [resolvedTheme]
- )
- const isPartnerBilledOrganization = isPartnerBillingOrganization(
- selectedOrganization?.billing_partner
- )
- return (
- <ScaffoldSection>
- <ScaffoldSectionDetail>
- <div className="sticky space-y-2 top-12 pr-3">
- <p className="text-foreground text-base m-0">Billing Address & Tax ID</p>
- <p className="text-sm text-foreground-light m-0">
- Changes will be reflected in every upcoming invoice, past invoices are not affected
- </p>
- <p className="text-sm text-foreground-light m-0">
- A Tax ID is only required for registered businesses.
- </p>
- </div>
- </ScaffoldSectionDetail>
- <ScaffoldSectionContent>
- {selectedOrganization && isPartnerBilledOrganization ? (
- <PartnerManagedResource
- managedBy={selectedOrganization?.managed_by}
- resource="Billing Addresses"
- cta={{
- installationId: selectedOrganization?.partner_id,
- }}
- />
- ) : isPermissionsLoaded && !canReadBillingCustomerData ? (
- <NoPermission resourceText="view this organization's billing address" />
- ) : (
- <>
- {(isLoading || isLoadingTaxId) && (
- <div className="space-y-2">
- <ShimmeringLoader />
- <ShimmeringLoader className="w-3/4" />
- <ShimmeringLoader className="w-1/2" />
- </div>
- )}
- {(error || errorLoadingTaxId) && (
- <AlertError
- subject="Failed to retrieve organization customer profile"
- error={(error || errorLoadingTaxId) as any}
- />
- )}
- {isSuccess && loadedTaxId && (
- <Elements stripe={stripePromise} options={stripeElementsOptions}>
- <Card>
- <Form {...form}>
- <form onSubmit={onFormSubmit}>
- <BillingCustomerDataForm
- className="p-8"
- form={form}
- disabled={!canUpdateBillingCustomerData}
- addressOptions={addressOptions}
- resetKey={resetKey}
- onAddressChange={onAddressChange}
- onAddressReady={(element) => {
- addressElementRef.current = element
- }}
- addressCountry={addressCountry}
- />
- <CardFooter className="border-t justify-end px-8">
- {!canUpdateBillingCustomerData && (
- <span className="text-sm text-foreground-lighter mr-auto">
- You need additional permissions to manage this organization's billing
- address
- </span>
- )}
- <div className="flex items-center gap-2">
- <Button type="default" onClick={handleReset} disabled={isSubmitDisabled}>
- Cancel
- </Button>
- <Button
- type="primary"
- htmlType="submit"
- disabled={isSubmitDisabled}
- loading={isSubmitting}
- >
- Save
- </Button>
- </div>
- </CardFooter>
- </form>
- </Form>
- </Card>
- </Elements>
- )}
- </>
- )}
- </ScaffoldSectionContent>
- </ScaffoldSection>
- )
- }
|