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

Merge pull request #1363 from porter-dev/nico/new-onboarding-flow

Styling + onboarding fixes
abelanger5 4 лет назад
Родитель
Сommit
c1f2be66d6
20 измененных файлов с 992 добавлено и 193 удалено
  1. 2 1
      api/types/project.go
  2. 10 1
      dashboard/src/components/ProvisionerStatus.tsx
  3. 8 0
      dashboard/src/main/home/onboarding/state/StateHandler.ts
  4. 0 2
      dashboard/src/main/home/onboarding/state/StepHandler.ts
  5. 1 1
      dashboard/src/main/home/onboarding/state/index.ts
  6. 1 1
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx
  7. 198 41
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_AWSRegistryForm.tsx
  8. 74 17
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx
  9. 182 22
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx
  10. 1 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/FormFlow.tsx
  11. 1 6
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/SharedStatus.tsx
  12. 212 41
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvisionerForm.tsx
  13. 73 15
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_DOProvisionerForm.tsx
  14. 200 22
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx
  15. 4 1
      dashboard/src/main/home/project-settings/BillingPage.tsx
  16. 2 2
      dashboard/src/main/home/provisioner/DOFormSection.tsx
  17. 2 2
      dashboard/src/shared/common.tsx
  18. 3 2
      ee/api/server/handlers/billing/get_token.go
  19. 15 12
      ee/billing/ironplans.go
  20. 3 3
      internal/billing/billing.go

+ 2 - 1
api/types/project.go

@@ -63,7 +63,8 @@ type DeleteRoleResponse struct {
 }
 
 type GetBillingTokenResponse struct {
-	Token string `json:"token"`
+	Token  string `json:"token"`
+	TeamID string `json:"team_id"`
 }
 
 type GetProjectBillingResponse struct {

+ 10 - 1
dashboard/src/components/ProvisionerStatus.tsx

@@ -98,11 +98,20 @@ const ProvisionerStatus: React.FC<Props> = ({ modules }) => {
 
       if (val.global_errors) {
         for (let globalErr of val.global_errors) {
-          errors.push("Global error: " + globalErr.error_context);
+          errors.push(globalErr.error_context);
           hasError = true;
         }
       }
 
+      // remove duplicate errors
+      errors = errors.filter(
+        (error, index, self) =>
+          index ===
+          self.findIndex(
+            (e) => e === error || e.includes(error) || error.includes(e)
+          )
+      );
+
       const width =
         val.status == "created"
           ? 100

+ 8 - 0
dashboard/src/main/home/onboarding/state/StateHandler.ts

@@ -77,6 +77,10 @@ export const StateHandler = proxy({
       };
     },
     saveRegistryProvider: (provider: string) => {
+      if (provider === StateHandler.connected_registry?.provider) {
+        return;
+      }
+
       StateHandler.connected_registry = {
         skip: false,
         provider: provider as any,
@@ -101,6 +105,10 @@ export const StateHandler = proxy({
       };
     },
     saveResourceProvisioningProvider: (provider: string) => {
+      if (provider === StateHandler.provision_resources?.provider) {
+        return;
+      }
+
       StateHandler.provision_resources = {
         skip: provider === "external",
         provider: provider as any,

+ 0 - 2
dashboard/src/main/home/onboarding/state/StepHandler.ts

@@ -72,7 +72,6 @@ const flow: FlowType = {
           execute: {
             on: {
               continue: "saveRegistryCredentials",
-              go_back: "clearRegistryProvider",
             },
           },
         },
@@ -143,7 +142,6 @@ const flow: FlowType = {
           execute: {
             on: {
               continue: "saveResourceProvisioningCredentials",
-              go_back: "clearResourceProvisioningProvider",
             },
           },
         },

+ 1 - 1
dashboard/src/main/home/onboarding/state/index.ts

@@ -101,7 +101,7 @@ const decompressState = (prev_state: any) => {
     skip: state.skip_registry_connection,
     provider: state.registry_connection_provider,
     credentials: {
-      id: state?.registry_connection_data?.id,
+      id: state?.registry_connection_credential_id,
     },
     settings: {
       registry_connection_id: state?.registry_connection_id,

+ 1 - 1
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx

@@ -52,7 +52,7 @@ const FormTitle = {
     icon: integrationList["gcr"].icon,
   },
   do: {
-    label: "Digital Ocean Container Registry (DOCR)",
+    label: "DigitalOcean Container Registry (DOCR)",
     icon: integrationList["do"].icon,
   },
 };

+ 198 - 41
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_AWSRegistryForm.tsx

@@ -3,7 +3,7 @@ import SelectRow from "components/form-components/SelectRow";
 import Helper from "components/form-components/Helper";
 import SaveButton from "components/SaveButton";
 import { AWSRegistryConfig } from "main/home/onboarding/types";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import api from "shared/api";
 import { useSnapshot } from "valtio";
@@ -11,6 +11,7 @@ import { OFState } from "../../../state/index";
 import IntegrationCategories from "main/home/integrations/IntegrationCategories";
 import { StateHandler } from "main/home/onboarding/state/StateHandler";
 import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
+import Loading from "components/Loading";
 
 const regionOptions = [
   { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
@@ -35,14 +36,61 @@ const regionOptions = [
   { value: "sa-east-1", label: "South America (São Paulo) sa-east-1" },
 ];
 
+const readableDate = (s: string) => {
+  const ts = new Date(s);
+  const date = ts.toLocaleDateString();
+  const time = ts.toLocaleTimeString([], {
+    hour: "numeric",
+    minute: "2-digit",
+  });
+  return `${time} on ${date}`;
+};
+
 export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<AWSRegistryConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(OFState);
   const [accessId, setAccessId] = useState("");
   const [secretKey, setSecretKey] = useState("");
   const [buttonStatus, setButtonStatus] = useState("");
   const [awsRegion, setAWSRegion] = useState("us-east-1");
+  const [showForm, setShowForm] = useState(false);
+  const [lastConnectedAccount, setLastConnectedAccount] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+    api
+      .getAWSIntegration("<token>", {}, { project_id: project.id })
+      .then((res) => {
+        let integrations = res.data;
+        if (!Array.isArray(integrations) || !integrations.length) {
+          setShowForm(true);
+          return;
+        }
+
+        let lastUsed = integrations.find((i) => {
+          return (
+            i.id === snap.StateHandler?.connected_registry?.credentials?.id
+          );
+        });
+
+        if (!lastUsed) {
+          setShowForm(true);
+          return;
+        }
+
+        setLastConnectedAccount(lastUsed);
+        setShowForm(false);
+      })
+      .catch((err) => {
+        setShowForm(true);
+        console.error(err);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  }, []);
 
   const validate = () => {
     if (!accessId) {
@@ -82,56 +130,114 @@ export const CredentialsForm: React.FC<{
         }
       );
 
-      nextFormStep({
-        credentials: {
-          id: res.data?.id,
-        },
-      });
+      continueToNextStep(res.data?.id);
     } catch (error) {
       setButtonStatus("Something went wrong, please try again");
     }
   };
 
+  const continueToNextStep = (integration_id: number) => {
+    nextFormStep({
+      credentials: {
+        id: integration_id,
+      },
+    });
+  };
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  if (showForm) {
+    return (
+      <>
+        <InputRow
+          type="text"
+          value={accessId}
+          setValue={(x: string) => {
+            setAccessId(x);
+          }}
+          label="👤 AWS Access ID"
+          placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+          width="100%"
+          isRequired={true}
+        />
+        <InputRow
+          type="password"
+          value={secretKey}
+          setValue={(x: string) => {
+            setSecretKey(x);
+          }}
+          label="🔒 AWS Secret Key"
+          placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+          width="100%"
+          isRequired={true}
+        />
+        <SelectRow
+          options={regionOptions}
+          width="100%"
+          scrollBuffer={true}
+          value={awsRegion}
+          dropdownMaxHeight="240px"
+          setActiveValue={(x: string) => {
+            setAWSRegion(x);
+          }}
+          label="📍 AWS Region"
+        />
+        <Br />
+        <Flex>
+          {lastConnectedAccount && (
+            <SaveButton
+              text="Cancel"
+              disabled={false}
+              onClick={() => setShowForm(false)}
+              makeFlush={true}
+              clearPosition={true}
+              status=""
+              statusPosition="right"
+              color="#fc4976"
+            />
+          )}
+          <SubmitButton
+            text="Continue"
+            disabled={false}
+            onClick={submit}
+            makeFlush={true}
+            clearPosition={true}
+            status={buttonStatus}
+            statusPosition={"right"}
+            disableLeftMargin={!lastConnectedAccount}
+          />
+        </Flex>
+      </>
+    );
+  }
+
   return (
     <>
-      <InputRow
-        type="text"
-        value={accessId}
-        setValue={(x: string) => {
-          setAccessId(x);
-        }}
-        label="👤 AWS Access ID"
-        placeholder="ex: AKIAIOSFODNN7EXAMPLE"
-        width="100%"
-        isRequired={true}
-      />
-      <InputRow
-        type="password"
-        value={secretKey}
-        setValue={(x: string) => {
-          setSecretKey(x);
-        }}
-        label="🔒 AWS Secret Key"
-        placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
-        width="100%"
-        isRequired={true}
-      />
-      <SelectRow
-        options={regionOptions}
-        width="100%"
-        scrollBuffer={true}
-        value={awsRegion}
-        dropdownMaxHeight="240px"
-        setActiveValue={(x: string) => {
-          setAWSRegion(x);
-        }}
-        label="📍 AWS Region"
-      />
+      <Helper>Connected account:</Helper>
+      <PreviewRow>
+        <Flex>
+          <i className="material-icons">account_circle</i>
+          {lastConnectedAccount.aws_arn || "arn: n/a"}
+        </Flex>
+        <Right>
+          Connected at{" "}
+          {readableDate(lastConnectedAccount.created_at)}
+        </Right>
+      </PreviewRow>
+      <Helper>
+        Want to use a different account?{" "}
+        <A onClick={() => setShowForm(true)} href="#">
+          Connect another account
+        </A>
+        .
+      </Helper>
       <Br />
       <SaveButton
         text="Continue"
         disabled={false}
-        onClick={submit}
+        onClick={() => continueToNextStep(lastConnectedAccount?.id)}
         makeFlush={true}
         clearPosition={true}
         status={buttonStatus}
@@ -194,7 +300,9 @@ export const SettingsForm: React.FC<{
 
   return (
     <>
-      <Helper>Provide a name for Porter to use when displaying your registry.</Helper>
+      <Helper>
+        Provide a name for Porter to use when displaying your registry.
+      </Helper>
       <InputRow
         type="text"
         value={registryName}
@@ -244,7 +352,56 @@ export const TestRegistryConnection: React.FC<{ nextFormStep: () => void }> = ({
   );
 };
 
+const Right = styled.div`
+  text-align: right;
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 15px;
 `;
+
+const A = styled.a`
+  cursor: pointer;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;
+
+const FlexColumn = styled.div`
+  display: flex;
+  flex-direction: column;
+`;
+
+const FlexColumnWithMargin = styled(FlexColumn)`
+  margin-left: ${(props: { marginLeft: string }) => props.marginLeft};
+`;
+
+const SelectableSpan = styled.span`
+  user-select: text;
+`;
+
+const PreviewRow = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff11;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+`;
+
+const SubmitButton = styled(SaveButton)`
+  margin-left: ${(props: { disableLeftMargin: boolean }) =>
+    props.disableLeftMargin ? "" : "16px"};
+`;

+ 74 - 17
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx

@@ -76,26 +76,52 @@ export const CredentialsForm: React.FC<{
     return <Loading />;
   }
 
+  let content = "Project name: n/a";
+
+  if (connectedAccount?.target_email) {
+    content = `${connectedAccount?.target_email}`;
+  }
+
+  if (connectedAccount?.target_id) {
+    content = `${connectedAccount?.target_id}`;
+  }
+
   return (
     <>
       {connectedAccount !== null && (
-        <div>
-          <div>Connected account: {connectedAccount.client}</div>
-          <div>Connected at: {readableDate(connectedAccount.created_at)}</div>
-        </div>
+        <>
+          <Helper>Connected account:</Helper>
+          <PreviewRow>
+            <Flex>
+              <i className="material-icons">account_circle</i>
+              {content}
+            </Flex>
+            <Right>Connected at {readableDate(connectedAccount.created_at)}</Right>
+          </PreviewRow>
+        </>
+      )}
+      {connectedAccount !== null ? (
+        <Helper>
+          Want to use a different account?{" "}
+          <A
+            href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
+          >
+            Sign in to DigitalOcean
+          </A>
+          .
+        </Helper>
+      ) : (
+        <ConnectDigitalOceanButton
+          href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
+        >
+          Sign In to DigitalOcean
+        </ConnectDigitalOceanButton>
       )}
-      <ConnectDigitalOceanButton
-        href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
-      >
-        {connectedAccount !== null
-          ? "Connect another account"
-          : "Sign In to Digital Ocean"}
-      </ConnectDigitalOceanButton>
 
-      <Br />
+      <Br height="5px" />
       {connectedAccount !== null && (
         <SaveButton
-          text="Continue with connected account"
+          text="Continue"
           disabled={false}
           onClick={() => submit(connectedAccount.id)}
           makeFlush={true}
@@ -153,7 +179,7 @@ export const SettingsForm: React.FC<{
         width="100%"
       />
       <Helper>
-        DOC R URI, in the form{" "}
+        DOCR URI, in the form{" "}
         <CodeBlock>registry.digitalocean.com/[REGISTRY_NAME]</CodeBlock>. For
         example, <CodeBlock>registry.digitalocean.com/porter-test</CodeBlock>.
       </Helper>
@@ -161,7 +187,7 @@ export const SettingsForm: React.FC<{
         type="text"
         value={registryUrl}
         setValue={(url: string) => setRegistryUrl(url)}
-        label="🔗 GCR URL"
+        label="🔗 DOCR URL"
         placeholder="ex: registry.digitalocean.com/porter-test"
         width="100%"
         isRequired={true}
@@ -205,9 +231,40 @@ export const TestRegistryConnection: React.FC<{
   );
 };
 
-const Br = styled.div`
+const A = styled.a`
+  cursor: pointer;
+`;
+
+const Right = styled.div`
+  text-align: right;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;
+
+const PreviewRow = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff11;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+`;
+
+const Br = styled.div<{ height?: string }>`
   width: 100%;
-  height: 15px;
+  height: ${(props) => props.height || "15px"};
 `;
 
 const CodeBlock = styled.span`

+ 182 - 22
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx

@@ -1,23 +1,71 @@
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import UploadArea from "components/form-components/UploadArea";
+import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 import { OFState } from "main/home/onboarding/state";
 import { StateHandler } from "main/home/onboarding/state/StateHandler";
 import { GCPRegistryConfig } from "main/home/onboarding/types";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
 
+const readableDate = (s: string) => {
+  const ts = new Date(s);
+  const date = ts.toLocaleDateString();
+  const time = ts.toLocaleTimeString([], {
+    hour: "numeric",
+    minute: "2-digit",
+  });
+  return `${time} on ${date}`;
+};
+
 export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(OFState);
   const [projectId, setProjectId] = useState("");
   const [serviceAccountKey, setServiceAccountKey] = useState("");
   const [buttonStatus, setButtonStatus] = useState("");
+  const [showForm, setShowForm] = useState(false);
+  const [lastConnectedAccount, setLastConnectedAccount] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+    api
+      .getGCPIntegration("<token>", {}, { project_id: project.id })
+      .then((res) => {
+        let integrations = res.data;
+        if (!Array.isArray(integrations) || !integrations.length) {
+          setShowForm(true);
+          return;
+        }
+
+        let lastUsed = integrations.find((i) => {
+          return (
+            i.id === snap.StateHandler?.connected_registry?.credentials?.id
+          );
+        });
+
+        if (!lastUsed) {
+          setShowForm(true);
+          return;
+        }
+
+        setLastConnectedAccount(lastUsed);
+        setShowForm(false);
+      })
+      .catch((err) => {
+        setShowForm(true);
+        console.error(err);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  }, []);
 
   const validate = () => {
     if (!projectId) {
@@ -63,34 +111,97 @@ export const CredentialsForm: React.FC<{
     }
   };
 
+  const continueToNextStep = (integration_id: number) => {
+    nextFormStep({
+      credentials: {
+        id: integration_id,
+      },
+    });
+  };
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  if (showForm) {
+    return (
+      <>
+        <InputRow
+          type="text"
+          value={projectId}
+          setValue={(x: string) => {
+            setProjectId(x);
+          }}
+          label="🏷️ GCP Project ID"
+          placeholder="ex: blindfold-ceiling-24601"
+          width="100%"
+          isRequired={true}
+        />
+
+        <Helper>Service account credentials for GCP permissions.</Helper>
+        <UploadArea
+          setValue={(x: any) => setServiceAccountKey(x)}
+          label="🔒 GCP Key Data (JSON)"
+          placeholder="Choose a file or drag it here."
+          width="100%"
+          height="100%"
+          isRequired={true}
+        />
+        <Br />
+        <Flex>
+          {lastConnectedAccount && (
+            <SaveButton
+              text="Cancel"
+              disabled={false}
+              onClick={() => setShowForm(false)}
+              makeFlush={true}
+              clearPosition={true}
+              status=""
+              statusPosition="right"
+              color="#fc4976"
+            />
+          )}
+          <SubmitButton
+            text="Continue"
+            disabled={false}
+            onClick={submit}
+            makeFlush={true}
+            clearPosition={true}
+            status={buttonStatus}
+            statusPosition={"right"}
+            disableLeftMargin={!lastConnectedAccount}
+          />
+        </Flex>
+      </>
+    );
+  }
   return (
     <>
-      <InputRow
-        type="text"
-        value={projectId}
-        setValue={(x: string) => {
-          setProjectId(x);
-        }}
-        label="🏷️ GCP Project ID"
-        placeholder="ex: blindfold-ceiling-24601"
-        width="100%"
-        isRequired={true}
-      />
+      <Helper>Connected account:</Helper>
+      <PreviewRow>
+        <Flex>
+          <i className="material-icons">account_circle</i>
+          {lastConnectedAccount?.gcp_sa_email}
+        </Flex>
+        <Right>
+          Connected at{" "}
+          {readableDate(lastConnectedAccount.created_at)}
+        </Right>
+      </PreviewRow>
+      <Helper>
+        Want to use a different account?{" "}
+        <A onClick={() => setShowForm(true)} href="#">
+          Connect another account
+        </A>
+        .
+      </Helper>
 
-      <Helper>Service account credentials for GCP permissions.</Helper>
-      <UploadArea
-        setValue={(x: any) => setServiceAccountKey(x)}
-        label="🔒 GCP Key Data (JSON)"
-        placeholder="Choose a file or drag it here."
-        width="100%"
-        height="100%"
-        isRequired={true}
-      />
       <Br />
+
       <SaveButton
         text="Continue"
         disabled={false}
-        onClick={submit}
+        onClick={() => continueToNextStep(lastConnectedAccount?.id)}
         makeFlush={true}
         clearPosition={true}
         status={buttonStatus}
@@ -228,6 +339,10 @@ export const TestRegistryConnection: React.FC<{
   );
 };
 
+const Right = styled.div`
+  text-align: right;
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 15px;
@@ -243,3 +358,48 @@ const CodeBlock = styled.span`
   margin-top: -2px;
   user-select: text;
 `;
+
+const A = styled.a`
+  cursor: pointer;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;
+
+const FlexColumn = styled.div`
+  display: flex;
+  flex-direction: column;
+`;
+
+const FlexColumnWithMargin = styled(FlexColumn)`
+  margin-left: ${(props: { marginLeft: string }) => props.marginLeft};
+`;
+
+const SelectableSpan = styled.span`
+  user-select: text;
+`;
+
+const PreviewRow = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff11;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+`;
+
+const SubmitButton = styled(SaveButton)`
+  margin-left: ${(props: { disableLeftMargin: boolean }) =>
+    props.disableLeftMargin ? "" : "16px"};
+`;

+ 1 - 1
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/FormFlow.tsx

@@ -47,7 +47,7 @@ const FormTitle = {
     icon: integrationList["gcp"].icon,
   },
   do: {
-    label: "Digital Ocean (DO)",
+    label: "DigitalOcean (DO)",
     icon: integrationList["do"].icon,
   },
   external: {

+ 1 - 6
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/SharedStatus.tsx

@@ -82,12 +82,6 @@ export const SharedStatus: React.FC<{
       ...globalErrors,
     ];
 
-    // remove duplicate global errors
-    tfModules[index].global_errors = tfModules[index].global_errors.filter(
-      (error, index, self) =>
-        index === self.findIndex((e) => e.error_context === error.error_context)
-    );
-
     setTFModules([...tfModules]);
   };
 
@@ -111,6 +105,7 @@ export const SharedStatus: React.FC<{
       ) {
         setInfraStatus({
           hasError: true,
+          description: "Encountered error while provisioning",
         });
         return;
       }

+ 212 - 41
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvisionerForm.tsx

@@ -7,10 +7,12 @@ import {
   AWSProvisionerConfig,
   AWSRegistryConfig,
 } from "main/home/onboarding/types";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import { useSnapshot } from "valtio";
 import { SharedStatus } from "./SharedStatus";
+import Loading from "components/Loading";
+import Helper from "components/form-components/Helper";
 
 const regionOptions = [
   { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
@@ -35,14 +37,77 @@ const regionOptions = [
   { value: "sa-east-1", label: "South America (São Paulo) sa-east-1" },
 ];
 
+const readableDate = (s: string) => {
+  const ts = new Date(s);
+  const date = ts.toLocaleDateString();
+  const time = ts.toLocaleTimeString([], {
+    hour: "numeric",
+    minute: "2-digit",
+  });
+  return `${time} on ${date}`;
+};
+
 export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<AWSRegistryConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(OFState);
   const [accessId, setAccessId] = useState("");
   const [secretKey, setSecretKey] = useState("");
   const [awsRegion, setAWSRegion] = useState("us-east-1");
   const [buttonStatus, setButtonStatus] = useState("");
+  const [showForm, setShowForm] = useState(false);
+  const [lastConnectedAccount, setLastConnectedAccount] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+    api
+      .getAWSIntegration("<token>", {}, { project_id: project.id })
+      .then((res) => {
+        let integrations = res.data;
+        if (!Array.isArray(integrations) || !integrations.length) {
+          setShowForm(true);
+          return;
+        }
+
+        // DO NOT USE THE INTEGRATION ID FROM THE CONNECTED REGISTRY
+        integrations = integrations.filter((i) => {
+          return (
+            i.id !== snap.StateHandler?.connected_registry?.credentials?.id
+          );
+        });
+
+        // filter can change the type from integrations so just in case
+        // we check again that integrations is an array
+        if (!Array.isArray(integrations) || !integrations) {
+          setShowForm(true);
+          return;
+        }
+
+        integrations.sort((a, b) => b.id - a.id);
+
+        let lastUsed = integrations.find((i) => {
+          return (
+            i.id === snap.StateHandler?.provision_resources?.credentials?.id
+          );
+        });
+
+        if (!lastUsed) {
+          setShowForm(true);
+          return;
+        }
+
+        setLastConnectedAccount(lastUsed);
+        setShowForm(false);
+      })
+      .catch((err) => {
+        setShowForm(true);
+        console.error(err);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  }, []);
 
   const validate = () => {
     if (!accessId) {
@@ -83,56 +148,113 @@ export const CredentialsForm: React.FC<{
         }
       );
 
-      nextFormStep({
-        credentials: {
-          id: res.data?.id,
-        },
-      });
+      continueToNextStep(res.data?.id);
     } catch (error) {
       setButtonStatus("Something went wrong, please try again");
     }
   };
 
+  const continueToNextStep = (integration_id: number) => {
+    nextFormStep({
+      credentials: {
+        id: integration_id,
+      },
+    });
+  };
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  if (showForm) {
+    return (
+      <div>
+        <InputRow
+          type="text"
+          value={accessId}
+          setValue={(x: string) => {
+            setAccessId(x);
+          }}
+          label="👤 AWS Access ID"
+          placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+          width="100%"
+          isRequired={true}
+        />
+        <InputRow
+          type="password"
+          value={secretKey}
+          setValue={(x: string) => {
+            setSecretKey(x);
+          }}
+          label="🔒 AWS Secret Key"
+          placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+          width="100%"
+          isRequired={true}
+        />
+        <SelectRow
+          options={regionOptions}
+          width="100%"
+          value={awsRegion}
+          scrollBuffer={true}
+          dropdownMaxHeight="240px"
+          setActiveValue={(x: string) => {
+            setAWSRegion(x);
+          }}
+          label="📍 AWS Region"
+        />
+        <Br />
+        <Flex>
+          {lastConnectedAccount && (
+            <SaveButton
+              text="Cancel"
+              disabled={false}
+              onClick={() => setShowForm(false)}
+              makeFlush={true}
+              clearPosition={true}
+              status=""
+              statusPosition="right"
+              color="#fc4976"
+            />
+          )}
+          <SubmitButton
+            text="Continue"
+            disabled={false}
+            onClick={submit}
+            makeFlush={true}
+            clearPosition={true}
+            status={buttonStatus}
+            statusPosition={"right"}
+            disableLeftMargin={!lastConnectedAccount}
+          />
+        </Flex>
+      </div>
+    );
+  }
+
   return (
     <>
-      <InputRow
-        type="text"
-        value={accessId}
-        setValue={(x: string) => {
-          setAccessId(x);
-        }}
-        label="👤 AWS Access ID"
-        placeholder="ex: AKIAIOSFODNN7EXAMPLE"
-        width="100%"
-        isRequired={true}
-      />
-      <InputRow
-        type="password"
-        value={secretKey}
-        setValue={(x: string) => {
-          setSecretKey(x);
-        }}
-        label="🔒 AWS Secret Key"
-        placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
-        width="100%"
-        isRequired={true}
-      />
-      <SelectRow
-        options={regionOptions}
-        width="100%"
-        value={awsRegion}
-        scrollBuffer={true}
-        dropdownMaxHeight="240px"
-        setActiveValue={(x: string) => {
-          setAWSRegion(x);
-        }}
-        label="📍 AWS Region"
-      />
-      <Br />
+      <Helper>Connected account:</Helper>
+      <PreviewRow>
+        <Flex>
+          <i className="material-icons">account_circle</i>
+          {lastConnectedAccount.aws_arn || "arn: n/a"}
+        </Flex>
+        <Right>
+          Connected at{" "}
+          {readableDate(lastConnectedAccount.created_at)}
+        </Right>
+      </PreviewRow>
+      <Helper>
+        Want to use a different account?{" "}
+        <A onClick={() => setShowForm(true)} href="#">
+          Connect another account
+        </A>
+        .
+      </Helper>
       <SaveButton
         text="Continue"
         disabled={false}
-        onClick={submit}
+        onClick={() => continueToNextStep(lastConnectedAccount?.id)}
         makeFlush={true}
         clearPosition={true}
         status={buttonStatus}
@@ -311,7 +433,56 @@ export const SettingsForm: React.FC<{
   );
 };
 
+const Right = styled.div`
+  text-align: right;
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 15px;
 `;
+
+const SubmitButton = styled(SaveButton)`
+  margin-left: ${(props: { disableLeftMargin: boolean }) =>
+    props.disableLeftMargin ? "" : "16px"};
+`;
+
+const A = styled.a`
+  cursor: pointer;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;
+
+const FlexColumn = styled.div`
+  display: flex;
+  flex-direction: column;
+`;
+
+const FlexColumnWithMargin = styled(FlexColumn)`
+  margin-left: ${(props: { marginLeft: string }) => props.marginLeft};
+`;
+
+const SelectableSpan = styled.span`
+  user-select: text;
+`;
+
+const PreviewRow = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff11;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+`;

+ 73 - 15
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_DOProvisionerForm.tsx

@@ -98,26 +98,52 @@ export const CredentialsForm: React.FC<{
     return <Loading />;
   }
 
+  let content = "Project name: n/a";
+
+  if (connectedAccount?.target_email) {
+    content = `${connectedAccount?.target_email}`;
+  }
+
+  if (connectedAccount?.target_id) {
+    content = `${connectedAccount?.target_id}`;
+  }
+
   return (
     <>
       {connectedAccount !== null && (
-        <div>
-          <div>Connected account: {connectedAccount.client}</div>
-          <div>Connected at: {readableDate(connectedAccount.created_at)}</div>
-        </div>
+        <>
+          <Helper>Connected account:</Helper>
+          <PreviewRow>
+            <Flex>
+              <i className="material-icons">account_circle</i>
+              {content}
+            </Flex>
+            <Right>Connected at {readableDate(connectedAccount.created_at)}</Right>
+          </PreviewRow>
+        </>
+      )}
+      {connectedAccount !== null ? (
+        <Helper>
+          Want to use a different account?{" "}
+          <A
+            href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
+          >
+            Sign in to DigitalOcean
+          </A>
+          .
+        </Helper>
+      ) : (
+        <ConnectDigitalOceanButton
+          href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
+        >
+          Sign In to DigitalOcean
+        </ConnectDigitalOceanButton>
       )}
-      <ConnectDigitalOceanButton
-        href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
-      >
-        {connectedAccount !== null
-          ? "Connect another account"
-          : "Sign In to Digital Ocean"}
-      </ConnectDigitalOceanButton>
 
-      <Br />
+      <Br height="5px" />
       {connectedAccount !== null && (
         <SaveButton
-          text="Continue with connected account"
+          text="Continue"
           disabled={false}
           onClick={() => submit(connectedAccount.id)}
           makeFlush={true}
@@ -319,9 +345,41 @@ export const SettingsForm: React.FC<{
   );
 };
 
-const Br = styled.div`
+const Right = styled.div`
+  text-align: right;
+  margin-left: 10px;
+`;
+
+const A = styled.a`
+  cursor: pointer;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;
+
+const PreviewRow = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff11;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+`;
+
+const Br = styled.div<{ height?: string }>`
   width: 100%;
-  height: 15px;
+  height: ${(props) => props.height || "15px"};
 `;
 
 const CodeBlock = styled.span`

+ 200 - 22
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx

@@ -2,13 +2,14 @@ import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import SelectRow from "components/form-components/SelectRow";
 import UploadArea from "components/form-components/UploadArea";
+import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import { OFState } from "main/home/onboarding/state";
 import {
   GCPProvisionerConfig,
   GCPRegistryConfig,
 } from "main/home/onboarding/types";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
@@ -41,13 +42,76 @@ const regionOptions = [
   { value: "us-west4", label: "us-west4" },
 ];
 
+const readableDate = (s: string) => {
+  const ts = new Date(s);
+  const date = ts.toLocaleDateString();
+  const time = ts.toLocaleTimeString([], {
+    hour: "numeric",
+    minute: "2-digit",
+  });
+  return `${time} on ${date}`;
+};
+
 export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(OFState);
   const [projectId, setProjectId] = useState("");
   const [serviceAccountKey, setServiceAccountKey] = useState("");
   const [buttonStatus, setButtonStatus] = useState("");
+  const [showForm, setShowForm] = useState(false);
+  const [lastConnectedAccount, setLastConnectedAccount] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+    api
+      .getGCPIntegration("<token>", {}, { project_id: project.id })
+      .then((res) => {
+        let integrations = res.data;
+        if (!Array.isArray(integrations) || !integrations.length) {
+          setShowForm(true);
+          return;
+        }
+
+        // DO NOT USE THE INTEGRATION ID FROM THE CONNECTED REGISTRY
+        integrations = integrations.filter((i) => {
+          return (
+            i.id !== snap.StateHandler?.connected_registry?.credentials?.id
+          );
+        });
+
+        // filter can change the type from integrations so just in case
+        // we check again that integrations is an array
+        if (!Array.isArray(integrations) || !integrations) {
+          setShowForm(true);
+          return;
+        }
+
+        integrations.sort((a, b) => b.id - a.id);
+
+        let lastUsed = integrations.find((i) => {
+          return (
+            i.id === snap.StateHandler?.provision_resources?.credentials?.id
+          );
+        });
+
+        if (!lastUsed) {
+          setShowForm(true);
+          return;
+        }
+
+        setLastConnectedAccount(lastUsed);
+        setShowForm(false);
+      })
+      .catch((err) => {
+        setShowForm(true);
+        console.error(err);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  }, []);
 
   const validate = () => {
     if (!projectId) {
@@ -92,33 +156,98 @@ export const CredentialsForm: React.FC<{
       setButtonStatus("Something went wrong, please try again");
     }
   };
+
+  const continueToNextStep = (integration_id: number) => {
+    nextFormStep({
+      credentials: {
+        id: integration_id,
+      },
+    });
+  };
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  if (showForm) {
+    return (
+      <>
+        <InputRow
+          type="text"
+          value={projectId}
+          setValue={(x: string) => {
+            setProjectId(x);
+          }}
+          label="🏷️ GCP Project ID"
+          placeholder="ex: blindfold-ceiling-24601"
+          width="100%"
+          isRequired={true}
+        />
+
+        <Helper>Service account credentials for GCP permissions.</Helper>
+        <UploadArea
+          setValue={(x: any) => setServiceAccountKey(x)}
+          label="🔒 GCP Key Data (JSON)"
+          placeholder="Choose a file or drag it here."
+          width="100%"
+          height="100%"
+          isRequired={true}
+        />
+        <Flex>
+          {lastConnectedAccount && (
+            <SaveButton
+              text="Cancel"
+              disabled={false}
+              onClick={() => setShowForm(false)}
+              makeFlush={true}
+              clearPosition={true}
+              status=""
+              statusPosition="right"
+              color="#fc4976"
+            />
+          )}
+          <SubmitButton
+            text="Continue"
+            disabled={false}
+            onClick={submit}
+            makeFlush={true}
+            clearPosition={true}
+            status={buttonStatus}
+            statusPosition={"right"}
+            disableLeftMargin={!lastConnectedAccount}
+          />
+        </Flex>
+      </>
+    );
+  }
+
   return (
     <>
-      <InputRow
-        type="text"
-        value={projectId}
-        setValue={(x: string) => {
-          setProjectId(x);
-        }}
-        label="🏷️ GCP Project ID"
-        placeholder="ex: blindfold-ceiling-24601"
-        width="100%"
-        isRequired={true}
-      />
+      <Helper>Connected account:</Helper>
+      <PreviewRow>
+        <Flex>
+          <i className="material-icons">account_circle</i>
+          {lastConnectedAccount?.gcp_sa_email || "n/a"}
+        </Flex>
+        <Right>
+          Connected at{" "}
+          {readableDate(lastConnectedAccount.created_at)}
+        </Right>
+      </PreviewRow>
+      <Helper>
+        Want to use a different account?{" "}
+        <A onClick={() => setShowForm(true)} href="#">
+          Connect another account
+        </A>
+        .
+      </Helper>
+
+      <Br />
 
-      <Helper>Service account credentials for GCP permissions.</Helper>
-      <UploadArea
-        setValue={(x: any) => setServiceAccountKey(x)}
-        label="🔒 GCP Key Data (JSON)"
-        placeholder="Choose a file or drag it here."
-        width="100%"
-        height="100%"
-        isRequired={true}
-      />
       <SaveButton
         text="Continue"
         disabled={false}
-        onClick={submit}
+        onClick={() => continueToNextStep(lastConnectedAccount?.id)}
         makeFlush={true}
         clearPosition={true}
         status={buttonStatus}
@@ -286,6 +415,10 @@ export const SettingsForm: React.FC<{
   );
 };
 
+const Right = styled.div`
+  text-align: right;
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 15px;
@@ -301,3 +434,48 @@ const CodeBlock = styled.span`
   margin-top: -2px;
   user-select: text;
 `;
+
+const A = styled.a`
+  cursor: pointer;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;
+
+const FlexColumn = styled.div`
+  display: flex;
+  flex-direction: column;
+`;
+
+const FlexColumnWithMargin = styled(FlexColumn)`
+  margin-left: ${(props: { marginLeft: string }) => props.marginLeft};
+`;
+
+const SelectableSpan = styled.span`
+  user-select: text;
+`;
+
+const PreviewRow = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff11;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+`;
+
+const SubmitButton = styled(SaveButton)`
+  margin-left: ${(props: { disableLeftMargin: boolean }) =>
+    props.disableLeftMargin ? "" : "16px"};
+`;

+ 4 - 1
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -5,6 +5,7 @@ import { Context } from "shared/Context";
 
 function BillingPage() {
   const [customerToken, setCustomerToken] = useState("");
+  const [teamID, setTeamID] = useState("");
   const { currentProject, setCurrentError, queryUsage } = useContext(Context);
 
   useEffect(() => {
@@ -14,7 +15,9 @@ function BillingPage() {
       .then((res) => {
         if (isSubscripted) {
           const token = res?.data?.token;
+          const teamID = res?.data?.team_id;
           setCustomerToken(token);
+          setTeamID(teamID);
         }
       })
       .catch((err) => {
@@ -28,7 +31,7 @@ function BillingPage() {
 
   return (
     <div style={{ height: "1000px" }}>
-      <CustomerProvider token={customerToken}>
+      <CustomerProvider token={customerToken} teamId={teamID}>
         <PlanSelect
           theme={{
             base: {

+ 2 - 2
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -25,8 +25,8 @@ type PropsType = {
 };
 
 const provisionOptions = [
-  { value: "docr", label: "Digital Ocean Container Registry" },
-  { value: "doks", label: "Digital Ocean Kubernetes Service" },
+  { value: "docr", label: "DigitalOcean Container Registry" },
+  { value: "doks", label: "DigitalOcean Kubernetes Service" },
 ];
 
 const tierOptions = [

+ 2 - 2
dashboard/src/shared/common.tsx

@@ -8,8 +8,8 @@ export const infraNames: any = {
   eks: "Elastic Kubernetes Service (EKS)",
   gcr: "Google Container Registry (GCR)",
   gke: "Google Kubernetes Engine (GKE)",
-  docr: "Digital Ocean Container Registry",
-  doks: "Digital Ocean Kubernetes Service",
+  docr: "DigitalOcean Container Registry",
+  doks: "DigitalOcean Kubernetes Service",
 };
 
 export const integrationList: any = {

+ 3 - 2
ee/api/server/handlers/billing/get_token.go

@@ -52,7 +52,7 @@ func (c *BillingGetTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		}
 	}
 
-	token, err := c.Config().BillingManager.GetIDToken(proj.ID, user)
+	token, teamID, err := c.Config().BillingManager.GetIDToken(proj, user)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -60,6 +60,7 @@ func (c *BillingGetTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	}
 
 	c.WriteResult(w, r, &types.GetBillingTokenResponse{
-		Token: token,
+		Token:  token,
+		TeamID: teamID,
 	})
 }

+ 15 - 12
ee/billing/ironplans.go

@@ -186,13 +186,16 @@ func (c *Client) RemoveUserFromTeam(role *cemodels.Role) error {
 }
 
 // GetIDToken gets an id token for a user in a project, creating the ID token if necessary
-func (c *Client) GetIDToken(projectID uint, user *cemodels.User) (token string, err error) {
-	// attempt to read the user billing data from the
-	userBilling, err := c.repo.UserBilling().ReadUserBilling(projectID, user.ID)
+func (c *Client) GetIDToken(proj *cemodels.Project, user *cemodels.User) (token string, teamID string, err error) {
+	// attempt to get a team ID for the project
+	teamID, err = c.GetTeamID(proj)
+
+	// attempt to read the user billing data from the project
+	userBilling, err := c.repo.UserBilling().ReadUserBilling(proj.ID, user.ID)
 	notFound := errors.Is(err, gorm.ErrRecordNotFound)
 
 	if !notFound && err != nil {
-		return "", err
+		return "", "", err
 	}
 
 	if !notFound {
@@ -204,14 +207,14 @@ func (c *Client) GetIDToken(projectID uint, user *cemodels.User) (token string,
 
 			// if JWT token has not expired, return the token
 			if !isTokExpired {
-				return token, nil
+				return token, teamID, nil
 			}
 		}
 	}
 
 	req := &CreateIDTokenRequest{
 		Email:  user.Email,
-		UserID: fmt.Sprintf("%d-%d", projectID, user.ID),
+		UserID: fmt.Sprintf("%d-%d", proj.ID, user.ID),
 	}
 
 	resp := &CreateIDTokenResponse{}
@@ -219,38 +222,38 @@ func (c *Client) GetIDToken(projectID uint, user *cemodels.User) (token string,
 	err = c.postRequest("/customers/v1/token", req, resp)
 
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 
 	token = resp.Token
 
 	if notFound {
 		_, err := c.repo.UserBilling().CreateUserBilling(&models.UserBilling{
-			ProjectID: projectID,
+			ProjectID: proj.ID,
 			UserID:    user.ID,
 			Token:     []byte(token),
 		})
 
 		if err != nil {
-			return "", err
+			return "", "", err
 		}
 	} else {
 		_, err := c.repo.UserBilling().UpdateUserBilling(&models.UserBilling{
 			Model: &gorm.Model{
 				ID: userBilling.ID,
 			},
-			ProjectID:  projectID,
+			ProjectID:  proj.ID,
 			UserID:     user.ID,
 			Token:      []byte(token),
 			TeammateID: userBilling.TeammateID,
 		})
 
 		if err != nil {
-			return "", err
+			return "", "", err
 		}
 	}
 
-	return token, nil
+	return token, teamID, nil
 }
 
 // VerifySignature verifies a webhook signature based on hmac protocol

+ 3 - 3
internal/billing/billing.go

@@ -32,7 +32,7 @@ type BillingManager interface {
 
 	// GetIDToken retrieves a billing token for a user. The billing token can be exchanged
 	// to view billing information.
-	GetIDToken(projectID uint, user *models.User) (token string, err error)
+	GetIDToken(proj *models.Project, user *models.User) (token string, teamID string, err error)
 
 	// ParseProjectUsageFromWebhook parses the project usage from a webhook payload sent
 	// from a billing agent
@@ -69,8 +69,8 @@ func (n *NoopBillingManager) RemoveUserFromTeam(role *models.Role) error {
 	return nil
 }
 
-func (n *NoopBillingManager) GetIDToken(projectID uint, user *models.User) (token string, err error) {
-	return "", nil
+func (n *NoopBillingManager) GetIDToken(proj *models.Project, user *models.User) (token string, teamID string, err error) {
+	return "", "", nil
 }
 
 func (n *NoopBillingManager) ParseProjectUsageFromWebhook(payload []byte) (*models.ProjectUsage, error) {