Browse Source

Add frontend stripe integration changes

Mauricio Araujo 2 years ago
parent
commit
ab66884659

+ 36 - 0
dashboard/package-lock.json

@@ -16,6 +16,8 @@
         "@react-spring/web": "^9.6.1",
         "@sentry/react": "^6.13.2",
         "@sentry/tracing": "^6.13.2",
+        "@stripe/react-stripe-js": "^2.6.2",
+        "@stripe/stripe-js": "^3.0.10",
         "@tanstack/react-query": "^4.13.0",
         "@tanstack/react-query-devtools": "^4.13.5",
         "@visx/axis": "^3.3.0",
@@ -2944,6 +2946,27 @@
       "integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw==",
       "dev": true
     },
+    "node_modules/@stripe/react-stripe-js": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.6.2.tgz",
+      "integrity": "sha512-FSjNg4v7BiCfojvx25PQ8DugOa09cGk1t816R/DLI/lT+1bgRAYpMvoPirLT4ZQ3ev/0VDtPdWNaabPsLDTOMA==",
+      "dependencies": {
+        "prop-types": "^15.7.2"
+      },
+      "peerDependencies": {
+        "@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/@stripe/stripe-js": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-3.0.10.tgz",
+      "integrity": "sha512-CFRNha+aPXR8GrqJss2TbK1j4aSGZXQY8gx0hvaYiSp+dU7EK/Zs5uwFTSAgV+t8H4+jcZ/iBGajAvoMYOwy+A==",
+      "engines": {
+        "node": ">=12.16"
+      }
+    },
     "node_modules/@tanstack/match-sorter-utils": {
       "version": "8.7.6",
       "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.6.tgz",
@@ -20202,6 +20225,19 @@
       "integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw==",
       "dev": true
     },
+    "@stripe/react-stripe-js": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.6.2.tgz",
+      "integrity": "sha512-FSjNg4v7BiCfojvx25PQ8DugOa09cGk1t816R/DLI/lT+1bgRAYpMvoPirLT4ZQ3ev/0VDtPdWNaabPsLDTOMA==",
+      "requires": {
+        "prop-types": "^15.7.2"
+      }
+    },
+    "@stripe/stripe-js": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-3.0.10.tgz",
+      "integrity": "sha512-CFRNha+aPXR8GrqJss2TbK1j4aSGZXQY8gx0hvaYiSp+dU7EK/Zs5uwFTSAgV+t8H4+jcZ/iBGajAvoMYOwy+A=="
+    },
     "@tanstack/match-sorter-utils": {
       "version": "8.7.6",
       "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.6.tgz",

+ 3 - 1
dashboard/package.json

@@ -8,6 +8,8 @@
     "@loadable/component": "^5.15.2",
     "@material-ui/core": "^4.11.3",
     "@material-ui/lab": "^4.0.0-alpha.61",
+    "@stripe/stripe-js": "^3.0.10",
+    "@stripe/react-stripe-js": "^2.6.2",
     "@react-spring/web": "^9.6.1",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",
@@ -179,4 +181,4 @@
       "prettier --write"
     ]
   }
-}
+}

+ 1 - 0
dashboard/src/assets/credit-card.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="white" d="M64 32C28.7 32 0 60.7 0 96v32H576V96c0-35.3-28.7-64-64-64H64zM576 224H0V416c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V224zM112 352h64c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm112 16c0-8.8 7.2-16 16-16H368c8.8 0 16 7.2 16 16s-7.2 16-16 16H240c-8.8 0-16-7.2-16-16z"/></svg>

+ 78 - 0
dashboard/src/main/home/modals/BillingModal.tsx

@@ -0,0 +1,78 @@
+import React, { Component, useState, useEffect } from "react";
+import { loadStripe } from '@stripe/stripe-js';
+import styled from "styled-components";
+import {
+    Elements,
+} from '@stripe/react-stripe-js';
+import PaymentSetupForm from "./PaymentSetupForm";
+import backArrow from "assets/back_arrow.png";
+
+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'
+            }
+        ]
+    };
+
+    return (
+        <>
+            <div id="checkout">
+                <BackButton onClick={back}>
+                    <BackButtonImg src={backArrow} />
+                </BackButton>
+                <Elements stripe={stripePromise} options={options} appearance={appearance}>
+                    <PaymentSetupForm
+                        project_id={project_id}
+                        onCreate={onCreate}
+                        back={back}
+                    >
+                    </PaymentSetupForm>
+                </Elements>
+            </div>
+        </>
+
+    );
+}
+
+export default BillingModal;
+
+const BackButton = styled.div`
+  display: flex;
+  width: 36px;
+  cursor: pointer;
+  height: 36px;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const BackButtonImg = styled.img`
+  width: 16px;
+  opacity: 0.75;
+`;

+ 62 - 0
dashboard/src/main/home/modals/PaymentSetupForm.tsx

@@ -0,0 +1,62 @@
+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 styled from "styled-components";
+import Error from "components/porter/Error";
+
+const PaymentSetupForm = ({ project_id, onCreate }) => {
+    const stripe = useStripe();
+    const elements = useElements();
+
+    const [errorMessage, setErrorMessage] = useState(null);
+    const [loading, setLoading] = useState(false);
+
+    const handleSubmit = async () => {
+        if (!stripe || !elements) {
+            return null;
+        }
+
+        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 })
+
+        // Finally, confirm with Stripe so the payment method is saved
+        const clientSecret = resp.data.clientSecret;
+        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>
+    )
+};
+
+export default PaymentSetupForm;
+
+const SubmitButton = styled(SaveButton)`
+  position: initial;
+`;

+ 147 - 54
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -1,69 +1,162 @@
 import React, { useContext, useEffect, useState } from "react";
-import { CustomerProvider, PlanSelect } from "@ironplans/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 styled from "styled-components";
+import Icon from "components/porter/Icon";
+import trashIcon from "assets/trash.png"
+import cardIcon from "assets/credit-card.svg"
+
 import { Context } from "shared/Context";
+import BillingModal from "../modals/BillingModal";
+import Error from "components/porter/Error";
+import Loading from "components/Loading";
 
 function BillingPage() {
-  const [customerToken, setCustomerToken] = useState("");
-  const [teamID, setTeamID] = useState("");
-  const { currentProject, setCurrentError, queryUsage } = useContext(Context);
+  const { user, currentProject } = useContext(Context);
+
+  const [paymentMethods, setPaymentMethods] = useState([]);
+  const [shouldCreate, setShouldCreate] = useState(false);
+  const [deleteStatus, setDeleteStatus] = useState(false);
 
   useEffect(() => {
-    let isSubscripted = true;
-    api
-      .getCustomerToken("<token>", {}, { project_id: currentProject?.id })
-      .then((res) => {
-        if (isSubscripted) {
-          const token = res?.data?.token;
-          const teamID = res?.data?.team_id;
-          setCustomerToken(token);
-          setTeamID(teamID);
-        }
-      })
-      .catch((err) => {
-        setCurrentError(err);
-      });
-    return () => {
-      isSubscripted = false;
-      queryUsage();
-    };
-  }, [currentProject?.id]);
+    (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)
+    })();
+  }, []);
+
+  if (shouldCreate) {
+    return (
+      <BillingModal
+        onCreate={() => setShouldCreate(false)}
+        back={() => setShouldCreate(false)}
+        project_id={currentProject?.id}
+        defaultValues={null}
+      />
+    );
+  }
+
+  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))
+  }
 
   return (
     <div style={{ height: "1000px" }}>
-      <CustomerProvider token={customerToken} teamId={teamID}>
-        <PlanSelect
-          theme={{
-            base: {
-              customFont: "Work Sans",
-              fontFamily: '"Work Sans", sans-serif',
-              darkMode: "on",
-              colors: {
-                primary: "rgba(97, 111, 238, 0.8)",
-                secondary: "rgb(103, 108, 124)",
-                danger: "rgb(227, 54, 109)",
-                success: "rgb(56, 168, 138)",
-              },
-            },
-            card: {
-              backgroundColor: "rgb(38, 40, 47)",
-              boxShadow: "rgb(0 0 0 / 33%) 0px 4px 15px 0px",
-              borderRadius: "8px",
-              border: "2px solid rgba(158, 180, 255, 0)",
-            },
-            button: {
-              base: {
-                boxShadow: "rgb(0 0 0 / 19%) 0px 2px 5px 0px",
-                borderRadius: "5px",
-                fontSize: "14px",
-                fontWeight: "500",
-              },
-            },
-          }}
-        ></PlanSelect>
-      </CustomerProvider>
+      <BillingModalWrapper>
+        <Heading isAtTop={true}>Payment methods</Heading>
+        <Helper>
+          This displays all configured payment methods
+        </Helper>
+        <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>
+              )
+            })
+          }
+        </PaymentMethodListWrapper>
+        <SaveButtonContainer>
+          <SaveButton
+            makeFlush={true}
+            clearPosition={true}
+            onClick={() => setShouldCreate(true)}
+          >
+            <i className="material-icons">add</i>
+            Add Payment Method
+          </SaveButton>
+        </SaveButtonContainer>
+      </BillingModalWrapper>
     </div>
   );
 }
 
 export default BillingPage;
+
+const PaymentMethodListWrapper = styled.div`
+  width: 100%;
+  max-height: 500px;
+  overflow-y: auto;
+`;
+
+const BillingModalWrapper = styled.div`
+  width: 60%;
+  min-width: 600px;
+`;
+
+const SaveButtonContainer = styled.div`
+  position: relative;
+  margin-top: 20px;
+`;
+
+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;
+`;
+
+const Container = styled.div`
+  padding: 5px;
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+`;
+
+const PaymentMethodText = 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;
+`;

+ 9 - 11
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -28,6 +28,8 @@ import APITokensSection from "./APITokensSection";
 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 = {
@@ -80,6 +82,7 @@ function ProjectSettings(props: any) {
     //   label: "Billing",
     // });
     tabOpts.push({ value: "metadata", label: "Metadata" });
+
     if (props.isAuthorized("settings", "", ["get", "delete"])) {
       // if (this.context?.hasBillingEnabled) {
       //   tabOptions.push({
@@ -99,6 +102,11 @@ function ProjectSettings(props: any) {
         value: "additional-settings",
         label: "Additional settings",
       });
+
+      tabOpts.push({
+        value: "billing",
+        label: "Billing",
+      });
     }
 
     if (!_.isEqual(tabOpts, tabOptions)) {
@@ -172,17 +180,7 @@ function ProjectSettings(props: any) {
       return <APITokensSection />;
     } else if (currentTab === "billing") {
       return (
-        <Placeholder>
-          <Helper>
-            Visit the{" "}
-            <a
-              href={`/api/projects/${context.currentProject?.id}/billing/redirect`}
-            >
-              billing portal
-            </a>{" "}
-            to view plans.
-          </Helper>
-        </Placeholder>
+        <BillingPage></BillingPage>
       );
     } else {
       return (

+ 100 - 50
dashboard/src/shared/api.tsx

@@ -371,9 +371,8 @@ const getFeedEvents = baseApi<
   }
 >("GET", (pathParams) => {
   const { project_id, cluster_id, stack_name, page } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${
-    page || 1
-  }`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1
+    }`;
 });
 
 const createEnvironment = baseApi<
@@ -861,11 +860,9 @@ const detectBuildpack = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 
 const detectGitlabBuildpack = baseApi<
@@ -896,11 +893,9 @@ const getBranchContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 
 const getProcfileContents = baseApi<
@@ -916,11 +911,9 @@ const getProcfileContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 
 const getPorterYamlContents = baseApi<
@@ -936,11 +929,9 @@ const getPorterYamlContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
 });
 
 const parsePorterYaml = baseApi<
@@ -1000,32 +991,30 @@ const getBranchHead = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/head`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/head`;
 });
 
 const createApp = baseApi<
   | {
-      name: string;
-      deployment_target_id: string;
-      type: "github";
-      git_repo_id: number;
-      git_branch: string;
-      git_repo_name: string;
-      porter_yaml_path: string;
-    }
+    name: string;
+    deployment_target_id: string;
+    type: "github";
+    git_repo_id: number;
+    git_branch: string;
+    git_repo_name: string;
+    porter_yaml_path: string;
+  }
   | {
-      name: string;
-      deployment_target_id: string;
-      type: "docker-registry";
-      image: {
-        repository: string;
-        tag: string;
-      };
-    },
+    name: string;
+    deployment_target_id: string;
+    type: "docker-registry";
+    image: {
+      repository: string;
+      tag: string;
+    };
+  },
   {
     project_id: number;
     cluster_id: number;
@@ -2240,11 +2229,9 @@ const getEnvGroup = baseApi<
     version?: number;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${
-    pathParams.cluster_id
-  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
-    pathParams.version ? "&version=" + pathParams.version : ""
-  }`;
+  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
+    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
+    }`;
 });
 
 const getConfigMap = baseApi<
@@ -3437,7 +3424,63 @@ const removeStackEnvGroup = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
 );
 
-const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
+// Billing
+const checkBillingCustomerExists = baseApi<
+  {
+    user_email?: string,
+  },
+  {
+    project_id?: number;
+  }
+>(
+  "POST",
+  ({ project_id }) =>
+    `/api/projects/${project_id}/billing/customer`
+);
+const listPaymentMethod = baseApi<
+  {},
+  {
+    project_id?: number;
+  }
+>(
+  "GET",
+  ({ project_id }) =>
+    `/api/projects/${project_id}/billing/payment_method`
+);
+const addPaymentMethod = baseApi<
+  {},
+  {
+    project_id?: number;
+  }
+>(
+  "POST",
+  ({ project_id }) =>
+    `/api/projects/${project_id}/billing/payment_method`
+);
+const updatePaymentMethod = baseApi<
+  {},
+  {
+    project_id?: number;
+    payment_method_id: string;
+  }
+>(
+  "GET",
+  ({ project_id, payment_method_id }) =>
+    `/api/projects/${project_id}/billing/payment_method/${payment_method_id}`
+);
+const deletePaymentMethod = baseApi<
+  {},
+  {
+    project_id?: number;
+    payment_method_id: string;
+  }
+>(
+  "DELETE",
+  ({ project_id, payment_method_id }) =>
+    `/api/projects/${project_id}/billing/payment_method/${payment_method_id}`
+);
+
+const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
 
 const createSecretAndOpenGitHubPullRequest = baseApi<
   {
@@ -3782,6 +3825,13 @@ export default {
   addStackEnvGroup,
   removeStackEnvGroup,
 
+  // BILLING
+  checkBillingCustomerExists,
+  listPaymentMethod,
+  addPaymentMethod,
+  updatePaymentMethod,
+  deletePaymentMethod,
+
   // STATUS
   getGithubStatus,
   getCloudProviderPermissionsStatus,

+ 3 - 0
zarf/helm/.dashboardenv

@@ -19,3 +19,6 @@ API_SERVER=http://localhost:8080
 # TRUST_ARN is used with the cloudformation pack, to allow supporting multiple AWS accounts as management accounts. Change MY_AWS_DEV_ACCOUNT_ID to your AWS developer account ID
 
 TRUST_ARN=arn:aws:iam::MY_AWS_DEV_ACCOUNT_ID:role/CAPIManagement
+
+# STRIPE_PUBLISHABLE_KEY is used to create Stripe Web Elements
+STRIPE_PUBLISHABLE_KEY=