Bläddra i källkod

Polish up frontend, add delete consent screen

Mauricio Araujo 2 år sedan
förälder
incheckning
bb1b874ac1

+ 2 - 0
api/types/billing.go

@@ -11,4 +11,6 @@ type PaymentMethod = struct {
 	ID           string `json:"id"`
 	DisplayBrand string `json:"display_brand"`
 	Last4        string `json:"last4"`
+	ExpMonth     int64  `json:"exp_month"`
+	ExpYear      int64  `json:"exp_year"`
 }

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

@@ -8,6 +8,8 @@ export const PaymentMethodValidator = z.object({
   display_brand: z.string(),
   id: z.string(),
   last4: z.string(),
+  exp_month: z.number(),
+  exp_year: z.number(),
 });
 
 export const ClientSecretResponse = z.string();

+ 1 - 3
dashboard/src/lib/hooks/useStripe.tsx

@@ -50,9 +50,7 @@ export const usePaymentMethods = (): TUsePaymentMethod => {
         { project_id: currentProject?.id }
       );
 
-      const data = await z
-        .array(PaymentMethodValidator)
-        .parseAsync(listResponse.data);
+      const data = PaymentMethodValidator.array().parse(listResponse.data);
       setPaymentMethodList(data);
 
       return data;

+ 62 - 44
dashboard/src/main/home/modals/BillingModal.tsx

@@ -1,57 +1,75 @@
-import React, { Component, useState, useEffect } from "react";
-import { loadStripe } from '@stripe/stripe-js';
+import React, { Component, useEffect, useState } from "react";
+import { Elements } from "@stripe/react-stripe-js";
+import { loadStripe } from "@stripe/stripe-js";
 import styled from "styled-components";
-import {
-    Elements,
-} from '@stripe/react-stripe-js';
-import PaymentSetupForm from "./PaymentSetupForm";
+
+import Heading from "components/form-components/Heading";
+
 import backArrow from "assets/back_arrow.png";
 
-const stripePromise = loadStripe(process.env.STRIPE_PUBLISHABLE_KEY || "")
+import PaymentSetupForm from "./PaymentSetupForm";
+
+const stripePromise = loadStripe(process.env.STRIPE_PUBLISHABLE_KEY || "");
 
 const BillingModal = ({ project_id, back, onCreate }) => {
-    const appearance = {
-        variables: {
-            colorPrimary: '#aaaabb',
-            colorBackground: "#27292e",
-            colorText: "#fefefe",
-            fontFamily: "Work Sans",
-        }
-    }
-    const options = {
-        mode: 'setup',
-        currency: 'usd',
-        setupFutureUsage: 'off_session',
-        paymentMethodTypes: ['card'],
-        appearance,
-        fonts: [
-            {
-                cssSrc: 'https://fonts.googleapis.com/css?family=Work+Sans'
-            }
-        ]
-    };
+  const { setCurrentModal } = useContext(Context);
 
-    return (
-        <>
-            <div id="checkout">
-                <BackButton onClick={back}>
-                    <BackButtonImg src={backArrow} />
-                </BackButton>
-                <Elements stripe={stripePromise} options={options} appearance={appearance}>
-                    <PaymentSetupForm
-                        projectId={project_id}
-                        onCreate={onCreate}
-                    >
-                    </PaymentSetupForm>
-                </Elements>
-            </div>
-        </>
+  const appearance = {
+    variables: {
+      colorPrimary: "#aaaabb",
+      colorBackground: "#27292e",
+      colorText: "#fefefe",
+      fontFamily: "Work Sans",
+    },
+  };
+  const options = {
+    mode: "setup",
+    currency: "usd",
+    setupFutureUsage: "off_session",
+    paymentMethodTypes: ["card"],
+    appearance,
+    fonts: [
+      {
+        cssSrc: "https://fonts.googleapis.com/css?family=Work+Sans",
+      },
+    ],
+  };
 
-    );
-}
+  return (
+    <>
+      <div id="checkout">
+        <ControlRow>
+          <Heading isAtTop={true}>Add Payment Method</Heading>
+          <BackButton onClick={back}>
+            <BackButtonImg src={backArrow} />
+          </BackButton>
+        </ControlRow>
+        <Elements
+          stripe={stripePromise}
+          options={options}
+          appearance={appearance}
+        >
+          <PaymentSetupForm
+            projectId={project_id}
+            onCreate={onCreate}
+          ></PaymentSetupForm>
+        </Elements>
+      </div>
+    </>
+  );
+};
 
 export default BillingModal;
 
+const ControlRow = styled.div`
+  width: 100%;
+  display: flex;
+  margin-left: auto;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 35px;
+`;
+
 const BackButton = styled.div`
   display: flex;
   width: 36px;

+ 52 - 0
dashboard/src/main/home/project-settings/BillingDeleteConsent.tsx

@@ -0,0 +1,52 @@
+import React, { useContext, useState } from "react";
+
+import Button from "components/porter/Button";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import { Context } from "shared/Context";
+
+type Props = {
+  setShowModal: (show: boolean) => void;
+  show: boolean;
+  onDelete: () => void;
+};
+
+const BillingDeleteConsent: React.FC<Props> = ({
+  setShowModal,
+  show,
+  onDelete,
+}) => {
+  const [confirmDelete, setDeleteCost] = useState("");
+  const { currentProject } = useContext(Context);
+  return show ? (
+    <>
+      <Modal
+        closeModal={() => {
+          setDeleteCost("");
+          setShowModal(false);
+        }}
+      >
+        <Text size={16}>Delete payment method?</Text>
+        <Spacer y={1} />
+        <Button
+          disabled={confirmDelete}
+          onClick={() => {
+            setShowModal(false);
+            onDelete();
+          }}
+          status={
+            confirmDelete == currentProject?.name
+              ? "This action cannot be undone"
+              : ""
+          }
+        >
+          Confirm
+        </Button>
+      </Modal>
+    </>
+  ) : null;
+};
+
+export default BillingDeleteConsent;

+ 22 - 26
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -13,9 +13,11 @@ import cardIcon from "assets/credit-card.svg";
 import trashIcon from "assets/trash.png";
 
 import BillingModal from "../modals/BillingModal";
+import BillingDeleteConsent from "./BillingDeleteConsent";
 
 function BillingPage() {
   const { currentProject } = useContext(Context);
+  const [showBillingDeleteModal, setShowBillingDeleteModal] = useState(false);
   const [shouldCreate, setShouldCreate] = useState(false);
   const {
     paymentMethodList,
@@ -29,10 +31,6 @@ function BillingPage() {
     refetchPaymentMethods();
   };
 
-  const onDelete = async (paymentMethodId: string) => {
-    deletePaymentMethod(paymentMethodId);
-  };
-
   if (shouldCreate) {
     return (
       <BillingModal
@@ -55,18 +53,27 @@ function BillingPage() {
                 <Container>
                   <Icon src={cardIcon} height={"14px"} />
                   <PaymentMethodText>
-                    {paymentMethod.display_brand} - **** **** ****{" "}
-                    {paymentMethod.last4}
+                    **** **** **** {paymentMethod.last4}
                   </PaymentMethodText>
+                  <ExpirationText>
+                    Expires: {paymentMethod.exp_month}/{paymentMethod.exp_year}
+                  </ExpirationText>
                   <DeleteButtonContainer>
                     {isDeleting ? (
                       <Loading />
                     ) : (
-                      <DeleteButton onClick={() => onDelete(paymentMethod.id)}>
+                      <DeleteButton
+                        onClick={() => setShowBillingDeleteModal(true)}
+                      >
                         <Icon src={trashIcon} height={"14px"} />
                       </DeleteButton>
                     )}
                   </DeleteButtonContainer>
+                  <BillingDeleteConsent
+                    setShowModal={setShowBillingDeleteModal}
+                    show={showBillingDeleteModal}
+                    onDelete={() => deletePaymentMethod(paymentMethod.id)}
+                  />
                 </Container>
               </PaymentMethodContainer>
             );
@@ -108,15 +115,14 @@ const SaveButtonContainer = styled.div`
 const PaymentMethodContainer = styled.div`
   color: #aaaabb;
   border-radius: 5px;
-  padding: 5px;
-  padding-left: 10px;
+  padding: 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;
+  margin-top: 10px;
 `;
 
 const Container = styled.div`
@@ -131,25 +137,15 @@ const PaymentMethodText = styled.span`
   margin-right: 5px;
 `;
 
+const ExpirationText = styled.span`
+  font-size: 0.8em;
+  margin-right: 5px;
+`;
+
 const DeleteButton = styled.div`
-  display: inline-block;
-  font-size: 13px;
-  font-weight: 500;
-  padding: 6px 10px;
-  text-align: center;
-  border: 1px solid #ffffff55;
-  border-radius: 4px;
-  background: #ffffff11;
-  color: #ffffffdd;
   cursor: pointer;
-  width: 120px;
-  :hover {
-    background: #ffffff22;
-  }
 `;
 
 const DeleteButtonContainer = styled.div`
-  width: 20%;
-  text-align: right;
-  margin-top: 12px;
+  text-align: center;
 `;

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

@@ -85,18 +85,17 @@ function ProjectSettings(props: any) {
         });
       }
 
-      tabOpts.push({
-        value: "additional-settings",
-        label: "Additional settings",
-      });
-
-      console.log("is billing enabled?", currentProject?.billing_enabled);
       // if (currentProject?.billing_enabled) {
       tabOpts.push({
         value: "billing",
         label: "Billing",
       });
       // }
+
+      tabOpts.push({
+        value: "additional-settings",
+        label: "Additional settings",
+      });
     }
 
     if (!_.isEqual(tabOpts, tabOptions)) {

+ 2 - 0
internal/billing/stripe.go

@@ -73,6 +73,8 @@ func (s *StripeBillingManager) ListPaymentMethod(proj *models.Project) (paymentM
 			ID:           stripePaymentMethod.ID,
 			DisplayBrand: stripePaymentMethod.Card.DisplayBrand,
 			Last4:        stripePaymentMethod.Card.Last4,
+			ExpMonth:     stripePaymentMethod.Card.ExpMonth,
+			ExpYear:      stripePaymentMethod.Card.ExpYear,
 		})
 	}