Просмотр исходного кода

Move stripe code to hooks, readd pointers

Mauricio Araujo 2 лет назад
Родитель
Сommit
17b36ea06e

+ 1 - 1
api/server/handlers/billing/create.go

@@ -33,7 +33,7 @@ func (c *CreateBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	ctx, span := telemetry.NewSpan(r.Context(), "auth-endpoint-api-token")
 	defer span.End()
 
-	proj, _ := ctx.Value(types.ProjectScope).(models.Project)
+	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	clientSecret, err := c.Config().BillingManager.CreatePaymentMethod(proj)
 	if err != nil {

+ 1 - 1
api/server/handlers/billing/list.go

@@ -32,7 +32,7 @@ func (c *ListBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "auth-endpoint-api-token")
 	defer span.End()
 
-	proj, _ := ctx.Value(types.ProjectScope).(models.Project)
+	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	paymentMethods, err := c.Config().BillingManager.ListPaymentMethod(proj)
 	if err != nil {

+ 15 - 0
dashboard/src/lib/billing/types.tsx

@@ -0,0 +1,15 @@
+import { z } from "zod";
+
+export type PaymentMethodList = {
+    paymentMethods: PaymentMethod[];
+};
+
+export type PaymentMethod = z.infer<typeof PaymentMethodValidator>;
+
+export const PaymentMethodValidator = z.object({
+    display_brand: z.string(),
+    id: z.string(),
+    last4: z.string(),
+});
+
+export const ClientSecretResponse = z.string();

+ 105 - 0
dashboard/src/lib/hooks/useStripe.tsx

@@ -0,0 +1,105 @@
+import { useContext, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { z } from "zod";
+
+import {
+  ClientSecretResponse,
+  PaymentMethodList,
+  PaymentMethodValidator,
+} from "lib/billing/types";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+type TCreatePaymentMethod = {
+  createPaymentMethod: () => Promise<string>;
+};
+
+type TDeletePaymentMethod = {
+  deletePaymentMethod: (paymentMethodId: string) => Promise<void>;
+  isDeleting: boolean;
+};
+
+export const usePaymentMethodList = (): PaymentMethodList => {
+  const { user, currentProject } = useContext(Context);
+  const clusterReq = useQuery(
+    ["getPaymentMethods", currentProject?.id],
+    async () => {
+      if (!currentProject?.id || currentProject.id === -1) {
+        return;
+      }
+      await api.checkBillingCustomerExists(
+        "<token>",
+        { user_email: user?.email },
+        { project_id: currentProject?.id }
+      );
+      const listResponse = await api.listPaymentMethod(
+        "<token>",
+        {},
+        { project_id: currentProject?.id }
+      );
+      const paymentMethodList = await z
+        .array(PaymentMethodValidator)
+        .parseAsync(listResponse.data);
+      return paymentMethodList;
+    },
+    {
+      refetchInterval: 3000,
+    }
+  );
+
+  return {
+    paymentMethods: clusterReq.data ?? [],
+  };
+};
+
+export const useCreatePaymentMethod = (): TCreatePaymentMethod => {
+  const { currentProject } = useContext(Context);
+
+  const createPaymentMethod = async () => {
+    const resp = await api.addPaymentMethod(
+      "<token>",
+      {},
+      { project_id: currentProject?.id }
+    );
+
+    const clientSecret = ClientSecretResponse.parse(resp.data);
+
+    return clientSecret;
+  };
+
+  return {
+    createPaymentMethod,
+  };
+};
+
+export const useDeletePaymentMethod = (): TDeletePaymentMethod => {
+  const { currentProject } = useContext(Context);
+  const [isDeleting, setIsDeleting] = useState<boolean>(false);
+
+  const deletePaymentMethod = async (paymentMethodId: string) => {
+    if (!currentProject?.id) {
+      throw new Error("Project ID is missing");
+    }
+    if (!paymentMethodId) {
+      throw new Error("Payment Method ID is missing");
+    }
+    setIsDeleting(true);
+
+    const resp = await api.deletePaymentMethod(
+      "<token>",
+      {},
+      { project_id: currentProject?.id, payment_method_id: paymentMethodId }
+    );
+    if (resp.status !== 200) {
+      throw new Error("Failed to delete payment method");
+    }
+
+    setIsDeleting(false);
+  };
+
+  return {
+    deletePaymentMethod,
+    isDeleting,
+  };
+};

+ 1 - 2
dashboard/src/main/home/modals/BillingModal.tsx

@@ -39,9 +39,8 @@ const BillingModal = ({ project_id, back, onCreate }) => {
                 </BackButton>
                 <Elements stripe={stripePromise} options={options} appearance={appearance}>
                     <PaymentSetupForm
-                        project_id={project_id}
+                        projectId={project_id}
                         onCreate={onCreate}
-                        back={back}
                     >
                     </PaymentSetupForm>
                 </Elements>

+ 65 - 52
dashboard/src/main/home/modals/PaymentSetupForm.tsx

@@ -1,58 +1,71 @@
-import React, { useState } from 'react';
-import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
-import api from 'shared/api';
-import SaveButton from "components/SaveButton";
+import React, { useState } from "react";
+import {
+  PaymentElement,
+  useElements,
+  useStripe,
+} from "@stripe/react-stripe-js";
 import styled from "styled-components";
+
 import Error from "components/porter/Error";
+import SaveButton from "components/SaveButton";
+import { useCreatePaymentMethod } from "lib/hooks/useStripe";
+
+const PaymentSetupForm = ({
+  onCreate,
+}: {
+  projectId: number;
+  onCreate: () => void;
+}) => {
+  const stripe = useStripe();
+  const elements = useElements();
+
+  const [errorMessage, setErrorMessage] = useState(null);
+  const [loading, setLoading] = useState(false);
+  const { createPaymentMethod } = useCreatePaymentMethod();
+
+  const handleSubmit = async () => {
+    if (!stripe || !elements) {
+      return;
+    }
+
+    setLoading(true);
+
+    // Submit form before calling the server
+    const { error: submitError } = await elements.submit();
+    if (submitError) {
+      setLoading(false);
+      return;
+    }
+
+    // Create the setup intent in the server
+    const clientSecret = await createPaymentMethod();
+
+    // Finally, confirm with Stripe so the payment method is saved
+    const { error } = await stripe.confirmSetup({
+      elements,
+      clientSecret,
+      redirect: "if_required",
+    });
+
+    if (error) {
+      setErrorMessage(error.message);
+    }
+
+    onCreate();
+  };
 
-const PaymentSetupForm = ({ projectId, onCreate }: { projectId: number, onCreate: () => void, }) => {
-    const stripe = useStripe();
-    const elements = useElements();
-
-    const [errorMessage, setErrorMessage] = useState(null);
-    const [loading, setLoading] = useState(false);
-
-    const handleSubmit = async () => {
-        if (!stripe || !elements) {
-            return;
-        }
-
-        setLoading(true);
-
-        // Submit form before calling the server
-        const { error: submitError } = await elements.submit();
-        if (submitError) {
-            setLoading(false);
-            return;
-        }
-
-        // Create the setup intent in the server
-        const resp = await api
-            .addPaymentMethod("<token>", {}, { project_id: projectId })
-
-        // Finally, confirm with Stripe so the payment method is saved
-        const clientSecret = resp.data;
-        const { error } = await stripe.confirmSetup({
-            elements,
-            clientSecret,
-            redirect: "if_required",
-        });
-
-        if (error) {
-            setErrorMessage(error.message);
-        }
-
-        onCreate()
-    };
-
-    return (
-        <form>
-            <PaymentElement />
-            <SubmitButton className='submit-button' text={"Add Payment Method"} disabled={!stripe || loading} onClick={handleSubmit}>
-            </SubmitButton>
-            {errorMessage && <Error message={errorMessage}></Error>}
-        </form>
-    )
+  return (
+    <form>
+      <PaymentElement />
+      <SubmitButton
+        className="submit-button"
+        text={"Add Payment Method"}
+        disabled={!stripe || loading}
+        onClick={handleSubmit}
+      ></SubmitButton>
+      {errorMessage && <Error message={errorMessage}></Error>}
+    </form>
+  );
 };
 
 export default PaymentSetupForm;

+ 55 - 71
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -1,53 +1,36 @@
-import React, { useContext, useEffect, useState } from "react";
-import api from "shared/api";
-import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
-import SaveButton from "components/SaveButton";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
+
+import Heading from "components/form-components/Heading";
+import Loading from "components/Loading";
 import Icon from "components/porter/Icon";
-import trashIcon from "assets/trash.png"
-import cardIcon from "assets/credit-card.svg"
+import Text from "components/porter/Text";
+import SaveButton from "components/SaveButton";
+import {
+  useDeletePaymentMethod,
+  usePaymentMethodList,
+} from "lib/hooks/useStripe";
 
 import { Context } from "shared/Context";
+import cardIcon from "assets/credit-card.svg";
+import trashIcon from "assets/trash.png";
+
 import BillingModal from "../modals/BillingModal";
-import Error from "components/porter/Error";
-import Loading from "components/Loading";
 
 function BillingPage() {
-  const { user, currentProject } = useContext(Context);
-
-  const [paymentMethods, setPaymentMethods] = useState([]);
+  const { currentProject } = useContext(Context);
   const [shouldCreate, setShouldCreate] = useState(false);
-  const [deleteStatus, setDeleteStatus] = useState(false);
 
-  useEffect(() => {
-    (async () => {
-      await api.checkBillingCustomerExists("<token>", { user_email: user?.email }, { project_id: currentProject?.id })
-      const listResponse = await api.listPaymentMethod("<token>", {}, { project_id: currentProject?.id })
-      const paymentMethodList = listResponse.data === null ? [] : listResponse.data
-      setPaymentMethods(paymentMethodList)
-    })();
-  }, []);
+  const { paymentMethods } = usePaymentMethodList();
+  const { deletePaymentMethod, isDeleting } = useDeletePaymentMethod();
 
   const onCreate = async () => {
-    // Refetch the payment method list since Stripe won't return the newly
-    // created payment method
-    const listResponse = await api.listPaymentMethod("<token>", {}, { project_id: currentProject?.id })
-    const paymentMethodList = listResponse.data === null ? [] : listResponse.data
-    setPaymentMethods(paymentMethodList)
-    setShouldCreate(false)
-  }
+    setShouldCreate(false);
+  };
 
-  const deletePaymentMethod = async (paymentMethod) => {
-    setDeleteStatus(true)
-    const resp = await api.deletePaymentMethod("<token>", {}, { project_id: currentProject?.id, payment_method_id: paymentMethod.id })
-    if (resp.status !== 200) {
-      return <Error message="failed to delete payment method" />
-    }
-
-    setDeleteStatus(false)
-    setPaymentMethods(paymentMethods.filter(elem => elem !== paymentMethod))
-  }
+  const onDelete = async (paymentMethodId: string) => {
+    deletePaymentMethod(paymentMethodId);
+  };
 
   if (shouldCreate) {
     return (
@@ -63,29 +46,30 @@ function BillingPage() {
     <div style={{ height: "1000px" }}>
       <BillingModalWrapper>
         <Heading isAtTop={true}>Payment methods</Heading>
-        <Helper>
-          This displays all configured payment methods
-        </Helper>
+        <Text>This displays all configured payment methods</Text>
         <PaymentMethodListWrapper>
-          {
-            paymentMethods.map((paymentMethod, idx) => {
-              return (
-                <PaymentMethodContainer key={idx}>
-                  <Container>
-                    <Icon src={cardIcon} height={"14px"} />
-                    <PaymentMethodText>{paymentMethod.card.display_brand} - **** **** **** {paymentMethod.card.last4}</PaymentMethodText>
-                    <DeleteButtonContainer>
-                      {
-                        deleteStatus ? <Loading /> : <DeleteButton onClick={() => deletePaymentMethod(paymentMethod)} status={deleteStatus}>
-                          <Icon src={trashIcon} height={"14px"} />
-                        </DeleteButton>
-                      }
-                    </DeleteButtonContainer>
-                  </Container>
-                </PaymentMethodContainer>
-              )
-            })
-          }
+          {paymentMethods.map((paymentMethod, idx) => {
+            return (
+              <PaymentMethodContainer key={idx}>
+                <Container>
+                  <Icon src={cardIcon} height={"14px"} />
+                  <PaymentMethodText>
+                    {paymentMethod.display_brand} - **** **** ****{" "}
+                    {paymentMethod.last4}
+                  </PaymentMethodText>
+                  <DeleteButtonContainer>
+                    {isDeleting ? (
+                      <Loading />
+                    ) : (
+                      <DeleteButton onClick={() => onDelete(paymentMethod.id)}>
+                        <Icon src={trashIcon} height={"14px"} />
+                      </DeleteButton>
+                    )}
+                  </DeleteButtonContainer>
+                </Container>
+              </PaymentMethodContainer>
+            );
+          })}
         </PaymentMethodListWrapper>
         <SaveButtonContainer>
           <SaveButton
@@ -121,17 +105,17 @@ const SaveButtonContainer = styled.div`
 `;
 
 const PaymentMethodContainer = styled.div`
-    color: #aaaabb;
-    border-radius: 5px;
-    padding: 5px;
-    padding-left: 10px;
-    display: block;
-    width: 100%;
-    border-radius: 5px;
-    background: ${(props) => props.theme.fg};
-    border: 1px solid ${({ theme }) => theme.border};
-    margin-bottom: 10px;
-    margin-top: 5px;
+  color: #aaaabb;
+  border-radius: 5px;
+  padding: 5px;
+  padding-left: 10px;
+  display: block;
+  width: 100%;
+  border-radius: 5px;
+  background: ${(props) => props.theme.fg};
+  border: 1px solid ${({ theme }) => theme.border};
+  margin-bottom: 10px;
+  margin-top: 5px;
 `;
 
 const Container = styled.div`

+ 2 - 6
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -25,11 +25,10 @@ import settingsGrad from "assets/settings-grad.svg";
 
 import DashboardHeader from "../cluster-dashboard/DashboardHeader";
 import APITokensSection from "./APITokensSection";
+import BillingPage from "./BillingPage";
 import InvitePage from "./InviteList";
 import Metadata from "./Metadata";
 import ProjectDeleteConsent from "./ProjectDeleteConsent";
-import BillingModal from "../modals/BillingModal";
-import BillingPage from "./BillingPage";
 
 type PropsType = RouteComponentProps & WithAuthProps & {};
 type ValidationError = {
@@ -97,7 +96,6 @@ function ProjectSettings(props: any) {
           label: "Billing",
         });
       }
-
     }
 
     if (!_.isEqual(tabOpts, tabOptions)) {
@@ -170,9 +168,7 @@ function ProjectSettings(props: any) {
     } else if (currentTab === "api-tokens") {
       return <APITokensSection />;
     } else if (currentTab === "billing") {
-      return (
-        <BillingPage></BillingPage>
-      );
+      return <BillingPage></BillingPage>;
     } else {
       return (
         <>

+ 16 - 17
ee/api/server/handlers/billing/webhook.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
-	"strconv"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -48,7 +47,7 @@ func (c *BillingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	}
 
 	// parse usage and update project
-	newUsage, features, err := c.Config().BillingManager.ParseProjectUsageFromWebhook(payload)
+	newUsage, err := c.Config().BillingManager.ParseProjectUsageFromWebhook(payload)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -88,25 +87,25 @@ func (c *BillingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		return
 	}
 
-	if managedDatabasesEnabled, err := strconv.ParseBool(features.ManagedDatabasesEnabled); err == nil {
-		project.RDSDatabasesEnabled = managedDatabasesEnabled
-	}
+	// if managedDatabasesEnabled, err := strconv.ParseBool(features.ManagedDatabasesEnabled); err == nil {
+	// 	project.RDSDatabasesEnabled = managedDatabasesEnabled
+	// }
 
-	if managedInfraEnabled, err := strconv.ParseBool(features.ManagedInfraEnabled); err == nil {
-		project.ManagedInfraEnabled = managedInfraEnabled
-	}
+	// if managedInfraEnabled, err := strconv.ParseBool(features.ManagedInfraEnabled); err == nil {
+	// 	project.ManagedInfraEnabled = managedInfraEnabled
+	// }
 
-	if stacksEnabled, err := strconv.ParseBool(features.StacksEnabled); err == nil {
-		project.StacksEnabled = stacksEnabled
-	}
+	// if stacksEnabled, err := strconv.ParseBool(features.StacksEnabled); err == nil {
+	// 	project.StacksEnabled = stacksEnabled
+	// }
 
-	if previewEnvsEnabled, err := strconv.ParseBool(features.PreviewEnvironmentsEnabled); err == nil {
-		project.PreviewEnvsEnabled = previewEnvsEnabled
-	}
+	// if previewEnvsEnabled, err := strconv.ParseBool(features.PreviewEnvironmentsEnabled); err == nil {
+	// 	project.PreviewEnvsEnabled = previewEnvsEnabled
+	// }
 
-	if capiProvisionerEnabled, err := strconv.ParseBool(features.CapiProvisionerEnabled); err == nil {
-		project.CapiProvisionerEnabled = capiProvisionerEnabled
-	}
+	// if capiProvisionerEnabled, err := strconv.ParseBool(features.CapiProvisionerEnabled); err == nil {
+	// 	project.CapiProvisionerEnabled = capiProvisionerEnabled
+	// }
 
 	_, err = c.Repo().Project().UpdateProject(project)
 

+ 2 - 2
internal/billing/billing.go

@@ -22,10 +22,10 @@ type BillingManager interface {
 	CreateCustomer(userEmail string, proj *models.Project) (customerID string, err error)
 
 	// ListPaymentMethod will return all payment methods for the project
-	ListPaymentMethod(proj models.Project) (paymentMethods []types.PaymentMethod, err error)
+	ListPaymentMethod(proj *models.Project) (paymentMethods []types.PaymentMethod, err error)
 
 	// CreatePaymentMethod will add a new payment method to the project in Stripe
-	CreatePaymentMethod(proj models.Project) (clientSecret string, err error)
+	CreatePaymentMethod(proj *models.Project) (clientSecret string, err error)
 
 	// DeletePaymentMethod will remove a payment method for the project in Stripe
 	DeletePaymentMethod(paymentMethodID string) (err error)

+ 2 - 2
internal/billing/stripe.go

@@ -42,7 +42,7 @@ func (s *StripeBillingManager) CreateCustomer(userEmail string, proj *models.Pro
 }
 
 // ListPaymentMethod will return all payment methods for the project
-func (s *StripeBillingManager) ListPaymentMethod(proj models.Project) (paymentMethods []types.PaymentMethod, err error) {
+func (s *StripeBillingManager) ListPaymentMethod(proj *models.Project) (paymentMethods []types.PaymentMethod, err error) {
 	stripe.Key = s.StripeSecretKey
 
 	params := &stripe.PaymentMethodListParams{
@@ -65,7 +65,7 @@ func (s *StripeBillingManager) ListPaymentMethod(proj models.Project) (paymentMe
 }
 
 // CreatePaymentMethod will add a new payment method to the project in Stripe
-func (s *StripeBillingManager) CreatePaymentMethod(proj models.Project) (clientSecret string, err error) {
+func (s *StripeBillingManager) CreatePaymentMethod(proj *models.Project) (clientSecret string, err error) {
 	stripe.Key = s.StripeSecretKey
 
 	params := &stripe.SetupIntentParams{