export function CheckoutForm()

in libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx [69:420]


export function CheckoutForm({
  cmsCommonContent,
  cart,
  locale,
}: CheckoutFormProps) {
  const { l10n } = useLocalization();
  const elements = useElements();
  const router = useRouter();
  const stripe = useStripe();
  const params = useParams();
  const searchParams = useSearchParams();

  const [formEnabled, setFormEnabled] = useState(false);
  const [showConsentError, setShowConsentError] = useState(false);
  const [isPaymentElementLoading, setIsPaymentElementLoading] = useState(true);
  const [loading, setLoading] = useState(false);
  const [stripeFieldsComplete, setStripeFieldsComplete] = useState(false);
  const [fullName, setFullName] = useState('');
  const [hasFullNameError, setHasFullNameError] = useState(false);
  const [selectedPaymentMethod, setSelectedPaymentMethod] = useState('');
  const [isSavedPaymentMethod, setIsSavedPaymentMethod] = useState(
    !!cart?.paymentInfo?.type
  );

  const engageGlean = useCallbackOnce(() => {
    recordEmitterEventAction(
      'checkoutEngage',
      { ...params },
      Object.fromEntries(searchParams)
    );
  }, []);

  useEffect(() => {
    if (elements) {
      const element = elements.getElement('payment');
      if (element) {
        element.on('ready', () => {
          setIsPaymentElementLoading(false);
        });

        element.on('change', (event: StripePaymentElementChangeEvent) => {
          if (event.complete) {
            setStripeFieldsComplete(true);
          } else {
            if (!stripeFieldsComplete) {
              setStripeFieldsComplete(false);
            }
          }

          //Show or hide the PayPal button
          setSelectedPaymentMethod(event?.value?.type || '');
          setIsSavedPaymentMethod(!!event?.value?.payment_method?.id);
        });
      } else {
        setIsPaymentElementLoading(false);
      }
    }
  }, [elements, stripeFieldsComplete]);

  const showPayPalButton = selectedPaymentMethod === 'external_paypal';
  const isStripe = cart?.paymentInfo?.type !== 'external_paypal';
  const showFullNameInput =
    !isPaymentElementLoading && !showPayPalButton && !isSavedPaymentMethod;
  const nonStripeFieldsComplete = !showFullNameInput || !!fullName;

  const submitHandler = async (
    event: React.SyntheticEvent<HTMLFormElement>
  ) => {
    event.preventDefault();

    if (!stripe || !elements) {
      // Stripe.js hasn't yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }

    setLoading(true);

    if (cart?.paymentInfo?.type === 'external_paypal') {
      recordEmitterEventAction(
        'checkoutSubmit',
        { ...params },
        Object.fromEntries(searchParams),
        'external_paypal'
      );

      await checkoutCartWithPaypal(cart.id, cart.version, {
        locale,
        displayName: '',
      });

      const queryParamString = searchParams.toString()
        ? `?${searchParams.toString()}`
        : '';
      router.push('./processing' + queryParamString);

      return;
    }

    if (showFullNameInput) {
      setHasFullNameError(!fullName);
      if (!fullName) {
        setLoading(false);
        return;
      }
    } else {
      setHasFullNameError(false);
    }

    // Trigger form validation and wallet collection
    const { error: submitError } = await elements.submit();
    if (submitError) {
      setLoading(false);
      return;
    }

    const confirmationTokenParams: ConfirmationTokenCreateParams | undefined =
      !isSavedPaymentMethod
        ? {
            payment_method_data: {
              billing_details: {
                name: fullName,
              },
            },
          }
        : undefined;

    // Create the ConfirmationToken using the details collected by the Payment Element
    // and additional shipping information
    const { error: confirmationTokenError, confirmationToken } =
      await stripe.createConfirmationToken({
        elements,
        params: confirmationTokenParams,
      });

    if (confirmationTokenError) {
      if (confirmationTokenError.type === 'validation_error') {
        return;
      } else {
        await handleStripeErrorAction(cart.id, confirmationTokenError);
        return;
      }
    }

    recordEmitterEventAction(
      'checkoutSubmit',
      { ...params },
      Object.fromEntries(searchParams),
      selectedPaymentMethod as PaymentProvidersType
    );

    await checkoutCartWithStripe(cart.id, cart.version, confirmationToken.id, {
      locale,
      displayName: fullName,
    });

    const queryParamString = searchParams.toString()
      ? `?${searchParams.toString()}`
      : '';
    router.push('./processing' + queryParamString);
  };

  return (
    <Form.Root
      aria-label="Checkout form"
      onSubmit={submitHandler}
      onChange={() => {
        engageGlean();
      }}
    >
      <CheckoutCheckbox
        isRequired={showConsentError}
        disabled={!cart.uid}
        termsOfService={cmsCommonContent.termsOfServiceUrl}
        privacyNotice={cmsCommonContent.privacyNoticeUrl}
        notifyCheckboxChange={(consentCheckbox) => {
          setFormEnabled(consentCheckbox);
          setShowConsentError(true);
        }}
      />
      <div
        className={
          formEnabled
            ? 'mt-10'
            : 'mt-10 cursor-not-allowed relative focus:border-blue-400 focus:outline-none focus:shadow-input-blue-focus after:absolute after:content-[""] after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:opacity-50 after:z-10'
        }
        onClick={() => setShowConsentError(true)}
      >
        {showFullNameInput && (
          <>
            <Localized id="next-new-user-card-title">
              <h3 className="font-semibold text-grey-600 text-start">
                Enter your card information
              </h3>
            </Localized>
            <Form.Field
              name="name"
              serverInvalid={hasFullNameError}
              className="my-6"
            >
              <Form.Label className="text-grey-400 block mb-1 text-start">
                <Localized id="payment-name-label">
                  Name as it appears on your card
                </Localized>
              </Form.Label>
              <Form.Control asChild>
                <input
                  className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
                  type="text"
                  data-testid="name"
                  placeholder={l10n.getString(
                    'payment-name-placeholder',
                    {},
                    'Full Name'
                  )}
                  readOnly={!formEnabled}
                  tabIndex={formEnabled ? 0 : -1}
                  value={fullName}
                  onChange={(e) => {
                    setFullName(e.target.value);
                    setHasFullNameError(!e.target.value);
                  }}
                  aria-required
                />
              </Form.Control>
              {hasFullNameError && (
                <Form.Message asChild>
                  <Localized id="next-payment-validate-name-error">
                    <p className="mt-1 text-alert-red" role="alert">
                      Please enter your name
                    </p>
                  </Localized>
                </Form.Message>
              )}
            </Form.Field>
          </>
        )}
        {cart?.paymentInfo?.type === 'external_paypal' ? (
          <div className="bg-white rounded-lg border border-[#e6e6e6] shadow-stripeBox">
            <h3 className="p-4 text-sm text-[#0570de] font-semibold">Saved</h3>
            <div className="p-4 pt-2 rounded-lg">
              <div className="bg-white p-4 rounded-lg border border-[#e6e6e6] shadow-stripeBox">
                <Image src={PaypalIcon} alt="paypal" />
              </div>
            </div>
          </div>
        ) : (
          <PaymentElement
            options={{
              layout: {
                type: 'accordion',
                defaultCollapsed: false,
                radios: false,
                spacedAccordionItems: true,
              },
              readOnly: !formEnabled,
              terms: {
                card: 'never',
              },
              defaultValues: {
                billingDetails: {
                  address: {
                    country: cart.taxAddress.countryCode,
                  },
                },
              },
            }}
          />
        )}
        {!isPaymentElementLoading && (
          <Form.Submit asChild>
            {showPayPalButton ? (
              <PayPalButtons
                style={{
                  layout: 'horizontal',
                  color: 'gold',
                  shape: 'rect',
                  label: 'paypal',
                  height: 48,
                  borderRadius: 6, // This should match 0.375rem
                  tagline: false,
                }}
                className="mt-6"
                createOrder={async () => getPayPalCheckoutToken(cart.currency)}
                onApprove={async (data: { orderID: string }) => {
                  await checkoutCartWithPaypal(
                    cart.id,
                    cart.version,
                    {
                      locale,
                      displayName: '',
                    },
                    data.orderID
                  );
                  const queryParamString = searchParams.toString()
                    ? `?${searchParams.toString()}`
                    : '';
                  router.push('./processing' + queryParamString);
                }}
                onError={async () => {
                  await finalizeCartWithError(
                    cart.id,
                    CartErrorReasonId.BASIC_ERROR
                  );
                  const queryParamString = searchParams.toString()
                    ? `?${searchParams.toString()}`
                    : '';

                  router.push('./error' + queryParamString);
                }}
                disabled={loading || !formEnabled}
              />
            ) : (
              <BaseButton
                className="mt-10 w-full"
                type="submit"
                variant={ButtonVariant.Primary}
                aria-disabled={
                  !formEnabled ||
                  (isStripe &&
                    !(stripeFieldsComplete && nonStripeFieldsComplete)) ||
                  loading
                }
              >
                {loading ? (
                  <Image
                    src={spinnerWhiteImage}
                    alt=""
                    className="absolute animate-spin h-8 w-8"
                  />
                ) : (
                  <>
                    <Image src={LockImage} className="h-4 w-4 mx-3" alt="" />
                    {isStripe ? (
                      <Localized id="next-new-user-submit">
                        Subscribe Now
                      </Localized>
                    ) : (
                      <Localized id="next-pay-with-heading-paypal">
                        Pay with PayPal
                      </Localized>
                    )}
                  </>
                )}
              </BaseButton>
            )}
          </Form.Submit>
        )}
      </div>
    </Form.Root>
  );
}