braintree / braintree-web

A suite of tools for integrating Braintree in the browser
https://developer.paypal.com/braintree/docs/start/hello-client/javascript/v3
MIT License
444 stars 134 forks source link

Cannot find module @/utils/braintree/braintree or its corresponding type declarations.ts(2307) #723

Open playfantasydraw opened 3 months ago

playfantasydraw commented 3 months ago

I have an app that uses Next.js. I attached my braintree.ts file. Also attached my Signup.tsx, where I want my Braintree payment methods to be displayed. And finally I attached my route.ts. My route.ts has the issue: Cannot find module @/utils/braintree/braintree or its corresponding type declarations.ts(2307).

Can you please advise on the best steps to solve this issue?

Signup.tsx

'use client';

import Button from '@/components/ui/Button';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { signUp } from '@/utils/auth-helpers/server';
import { handleRequest } from '@/utils/auth-helpers/client';
import { useRouter } from 'next/navigation';
import Script from 'next/script';

interface SignUpProps {
  allowEmail: boolean;
  redirectMethod: string;
}

export default function SignUp({ allowEmail, redirectMethod }: SignUpProps) {
  const router = redirectMethod === 'client' ? useRouter() : null;
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [showBraintree, setShowBraintree] = useState(false);
  const [braintreeInstance, setBraintreeInstance] = useState<any>(null);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsSubmitting(true);
    // Here, instead of calling handleRequest, we'll just show the Braintree UI
    setShowBraintree(true);
    setIsSubmitting(false);
  };

  useEffect(() => {
    if (showBraintree && (window as any).braintree) {
      (window as any).braintree.dropin.create({
        authorization: process.env.NEXT_PUBLIC_BRAINTREE_TOKENIZATION_KEY,
        container: '#dropin-container'
      }, (createErr: any, instance: any) => {
        if (createErr) {
          console.error(createErr);
          return;
        }
        setBraintreeInstance(instance);
      });
    }
  }, [showBraintree]);

  const handlePayment = async () => {
    if (!braintreeInstance) {
      console.error('Braintree instance not available');
      return;
    }

    try {
      const { nonce } = await braintreeInstance.requestPaymentMethod();
      console.log('Payment nonce:', nonce);
      // Here you would typically send the nonce to your server
      // along with the user's email and password to complete the sign-up process
      alert('Payment method successfully created! Sign-up would be completed here.');
      // After successful sign-up and payment, you might want to redirect the user
      if (router) router.push('/dashboard');
    } catch (error) {
      console.error('Payment error:', error);
      alert('Payment failed. Please try again.');
    }
  };

  return (
    <div className="my-8">
      <form
        noValidate={true}
        className="mb-4"
        onSubmit={(e) => handleSubmit(e)}
      >
        <div className="grid gap-2">
          <div className="grid gap-1">
            <label htmlFor="email">Email</label>
            <input
              id="email"
              placeholder="name@example.com"
              type="email"
              name="email"
              autoCapitalize="none"
              autoComplete="email"
              autoCorrect="off"
              className="w-full p-3 rounded-md bg-zinc-800"
            />
            <label htmlFor="password">Password</label>
            <input
              id="password"
              placeholder="Password"
              type="password"
              name="password"
              autoComplete="current-password"
              className="w-full p-3 rounded-md bg-zinc-800"
            />
          </div>
          <Button
            variant="slim"
            type="submit"
            className="mt-1"
            loading={isSubmitting}
          >
            Continue to Payment
          </Button>
        </div>
      </form>
      {showBraintree && (
        <div id="dropin-wrapper">
          <div id="dropin-container"></div>
          <Button onClick={handlePayment} variant="slim" className="mt-4">
            Complete Sign Up and Pay
          </Button>
        </div>
      )}
      <p>Already have an account?</p>
      <p>
        <Link href="/signin/password_signin" className="font-light text-sm">
          Sign in with email and password
        </Link>
      </p>
      {allowEmail && (
        <p>
          <Link href="/signin/email_signin" className="font-light text-sm">
            Sign in via magic link
          </Link>
        </p>
      )}
      <Script
        src="https://js.braintreegateway.com/web/dropin/1.33.0/js/dropin.min.js"
        strategy="lazyOnload"
      />
    </div>
  );
}

route.ts

import Stripe from 'stripe';
import { stripe } from '@/utils/stripe/config';
import {
  upsertProductRecord,
  upsertPriceRecord,
  manageSubscriptionStatusChange,
  deleteProductRecord,
  deletePriceRecord
} from '@/utils/supabase/admin';
import { gateway } from '@/utils/braintree/braintree';

const relevantEvents = new Set([
  'product.created',
  'product.updated',
  'product.deleted',
  'price.created',
  'price.updated',
  'price.deleted',
  'checkout.session.completed',
  'customer.subscription.created',
  'customer.subscription.updated',
  'customer.subscription.deleted'
]);

export async function POST(req: Request) {
  if (req.headers.get('Content-Type') === 'application/json') {
    const body = await req.json();
    const { paymentMethodNonce } = body;

    if (!paymentMethodNonce) {
      return new Response('Payment method nonce is required', { status: 400 });
    }

    try {
      const result = await gateway.transaction.sale({
        amount: '10.00',
        paymentMethodNonce: paymentMethodNonce,
        options: {
          submitForSettlement: true
        }
      });

      if (result.success) {
        return new Response(JSON.stringify({ success: true, transaction: result.transaction }), {
          status: 200,
          headers: { 'Content-Type': 'application/json' }
        });
      } else {
        return new Response(JSON.stringify({ success: false, error: result.message }), {
          status: 400,
          headers: { 'Content-Type': 'application/json' }
        });
      }
    } catch (error) {
      console.error('Braintree transaction error:', error);
      return new Response('An error occurred while processing the payment', { status: 500 });
    }
  } else {
    const body = await req.text();
    const sig = req.headers.get('stripe-signature') as string;
    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
    let event: Stripe.Event;

    try {
      if (!sig || !webhookSecret)
        return new Response('Webhook secret not found.', { status: 400 });
      event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
      console.log(`🔔  Webhook received: ${event.type}`);
    } catch (err: any) {
      console.log(`❌ Error message: ${err.message}`);
      return new Response(`Webhook Error: ${err.message}`, { status: 400 });
    }

    if (relevantEvents.has(event.type)) {
      try {
        switch (event.type) {
          case 'product.created':
          case 'product.updated':
            await upsertProductRecord(event.data.object as Stripe.Product);
            break;
          case 'price.created':
          case 'price.updated':
            await upsertPriceRecord(event.data.object as Stripe.Price);
            break;
          case 'price.deleted':
            await deletePriceRecord(event.data.object as Stripe.Price);
            break;
          case 'product.deleted':
            await deleteProductRecord(event.data.object as Stripe.Product);
            break;
          case 'customer.subscription.created':
          case 'customer.subscription.updated':
          case 'customer.subscription.deleted':
            const subscription = event.data.object as Stripe.Subscription;
            await manageSubscriptionStatusChange(
              subscription.id,
              subscription.customer as string,
              event.type === 'customer.subscription.created'
            );
            break;
          case 'checkout.session.completed':
            const checkoutSession = event.data.object as Stripe.Checkout.Session;
            if (checkoutSession.mode === 'subscription') {
              const subscriptionId = checkoutSession.subscription;
              await manageSubscriptionStatusChange(
                subscriptionId as string,
                checkoutSession.customer as string,
                true
              );
            }
            break;
          default:
            throw new Error('Unhandled relevant event!');
        }
      } catch (error) {
        console.log(error);
        return new Response(
          'Webhook handler failed. View your Next.js function logs.',
          {
            status: 400
          }
        );
      }
    } else {
      return new Response(`Unsupported event type: ${event.type}`, {
        status: 400
      });
    }
    return new Response(JSON.stringify({ received: true }));
  }
}

braintree.ts

import braintree from 'braintree';

export const gateway = new braintree.BraintreeGateway({
  environment: braintree.Environment.Sandbox,
  merchantId: process.env.BRAINTREE_MERCHANT_ID!,
  publicKey: process.env.BRAINTREE_PUBLIC_KEY!,
  privateKey: process.env.BRAINTREE_PRIVATE_KEY!
});