import HCaptcha from '@hcaptcha/react-hcaptcha' import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { Calendar, PartyPopper } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, DialogTrigger, Form, FormField, Input, Separator, } from 'ui' import { Admonition, ShimmeringLoader, TimestampInfo } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { z } from 'zod' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton' import { useOrganizationCreditCodeRedemptionMutation } from '@/data/organizations/organization-credit-code-redemption-mutation' import { useOrganizationQuery } from '@/data/organizations/organization-query' import { useOrgBalanceQuery } from '@/data/subscriptions/org-balance-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useLatest } from '@/hooks/misc/useLatest' const FORM_ID = 'credit-code-redemption' const FormSchema = z.object({ code: z.string().min(1, 'Code is required'), }) type CreditCodeRedemptionForm = z.infer export const CreditCodeRedemption = ({ slug, modalVisible = false, onClose, }: { slug?: string modalVisible?: boolean onClose?: () => void }) => { const router = useRouter() const [codeRedemptionModalVisible, setCodeRedemptionModalVisible] = useState( modalVisible || false ) const { data: org, isLoading: isOrgLoading } = useOrganizationQuery({ slug }) const { data: orgBalance, isLoading: isOrgBalanceLoading } = useOrgBalanceQuery( { orgSlug: slug }, { enabled: codeRedemptionModalVisible } ) const combinedCreditBalanceCents = orgBalance?.total_balance_cents const { can: canRedeemCode, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions', undefined, { organizationSlug: slug } ) const captchaRef = useRef(null) const captchaTokenRef = useRef(null) const codeRedemptionDisabled = !canRedeemCode || !isPermissionsLoaded || isOrgLoading || isOrgBalanceLoading const form = useForm({ resolver: zodResolver(FormSchema as any), defaultValues: { code: '' }, }) const { isValid } = form.formState const { mutate: redeemCode, isPending: redeemingCode, error: errorRedeemingCode, data: codeRedemptionResult, reset: resetCodeRedemption, } = useOrganizationCreditCodeRedemptionMutation({ onSuccess: () => { form.setValue('code', '') resetCaptcha() }, }) const resetCaptcha = () => { captchaTokenRef.current = null captchaRef.current?.resetCaptcha() } const initHcaptcha = async () => { let token = captchaTokenRef.current try { if (!token) { const captchaResponse = await captchaRef.current?.execute({ async: true }) token = captchaResponse?.response ?? null captchaTokenRef.current = token return token } } catch (error) { return token } return token } const initHcaptchaRef = useLatest(initHcaptcha) const onSubmit: SubmitHandler = async ({ code }) => { const token = await initHcaptcha() redeemCode({ slug, code, hcaptchaToken: token }) } const onCodeRedemptionDialogVisibilityChange = (visible: boolean) => { setCodeRedemptionModalVisible(visible) if (!visible) { resetCodeRedemption() resetCaptcha() onClose?.() } } useEffect(() => { if (!router.isReady) return const queryCode = router.query.code const codeFromParams = Array.isArray(queryCode) ? queryCode[0] : queryCode if (typeof codeFromParams === 'string' && codeFromParams.trim().length > 2) { form.setValue('code', codeFromParams) } }, [router.isReady, router.query.code, form]) useEffect(() => { if (codeRedemptionModalVisible) { initHcaptchaRef.current() } }, [codeRedemptionModalVisible, initHcaptchaRef]) return ( {!modalVisible && ( Redeem Code )} e.preventDefault()}> { // [Joshen] This is to ensure that hCaptcha popup remains clickable if (document !== undefined) document.body.classList.add('pointer-events-auto!') }} onClose={() => { if (document !== undefined) document.body.classList.remove('pointer-events-auto!') }} onVerify={(token) => { captchaTokenRef.current = token if (document !== undefined) document.body.classList.remove('pointer-events-auto!') }} onExpire={() => { captchaTokenRef.current = null }} /> {!!codeRedemptionResult ? (

Credits redeemed!

$

{codeRedemptionResult.amount_cents / 100}

credits applied

{codeRedemptionResult.credits_expire_at && (
Expires on{' '}
)} {(!router.pathname.includes('/org/') || org?.plan.id === 'free') && (
{org?.plan.id === 'free' && ( Upgrade organization )} {!router.pathname.includes('/org/') && ( )}
)}
) : ( <> Redeem Code Redeem your credit code to add credits to your organization
{isOrgLoading || isOrgBalanceLoading || !isPermissionsLoaded ? (
) : ( ( )} /> {combinedCreditBalanceCents !== undefined && combinedCreditBalanceCents > 0 && (
Current Balance

$

{combinedCreditBalanceCents / 100}

/credits

)}

Credits are applied to {org?.name} only and cannot be shared or transferred to other organizations. Credits are automatically used toward invoices.

When credits run out on a paid plan, your default payment method will be charged—your plan won't be downgraded automatically.

{errorRedeemingCode && ( )}
Redeem
)} )}
) }