From 635ad3a4e03bdfd5a465e68f703c4464687115eb Mon Sep 17 00:00:00 2001
From: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com>
Date: Wed, 25 Sep 2024 19:03:09 +0200
Subject: [PATCH] Pre fill non-sensitive user information during setup for
 hosted instances (#48019)

* Pre-fill cloud user info from search params

* Autofocus password field for hosted instances

* Add tests

* Parse setup user info and store it immediatelly

* Autofocus password input field for hosted instances

* Add e2e reproduction

* Fix the user info

* Fix unit test
---
 .../onboarding/setup/setup.cy.spec.ts         | 26 +++++++++++++++
 .../setup/components/UserForm/UserForm.tsx    |  8 ++++-
 .../setup/components/UserStep/UserStep.tsx    |  2 ++
 .../UserStep/UserStep.unit.spec.tsx           | 33 ++++++++++++++++++-
 frontend/src/metabase/setup/reducers.ts       | 20 ++++++++++-
 5 files changed, 86 insertions(+), 3 deletions(-)

diff --git a/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts b/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts
index b62e8df52a1..f0e34845999 100644
--- a/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts
+++ b/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts
@@ -8,9 +8,12 @@ import {
   expectNoBadSnowplowEvents,
   isEE,
   main,
+  mockSessionProperty,
+  onlyOnEE,
   popover,
   resetSnowplow,
   restore,
+  setTokenFeatures,
 } from "e2e/support/helpers";
 import { SUBSCRIBE_URL } from "metabase/setup/constants";
 
@@ -233,6 +236,29 @@ describe("scenarios > setup", () => {
     });
   });
 
+  it("should pre-fill user info for hosted instances (infra-frontend#1109)", () => {
+    onlyOnEE();
+    setTokenFeatures("none");
+    mockSessionProperty("is-hosted?", true);
+
+    cy.visit(
+      "/setup?first_name=John&last_name=Doe&email=john@doe.test&site_name=Doe%20Unlimited",
+    );
+
+    skipWelcomePage();
+    selectPreferredLanguageAndContinue();
+
+    cy.findByTestId("setup-forms").within(() => {
+      cy.findByDisplayValue("John").should("exist");
+      cy.findByDisplayValue("Doe").should("exist");
+      cy.findByDisplayValue("john@doe.test").should("exist");
+      cy.findByDisplayValue("Doe Unlimited").should("exist");
+      cy.findByLabelText("Create a password")
+        .should("be.focused")
+        .and("be.empty");
+    });
+  });
+
   it("should allow you to connect a db during setup", () => {
     const dbName = "SQLite db";
 
diff --git a/frontend/src/metabase/setup/components/UserForm/UserForm.tsx b/frontend/src/metabase/setup/components/UserForm/UserForm.tsx
index 246141a6011..85f7db74786 100644
--- a/frontend/src/metabase/setup/components/UserForm/UserForm.tsx
+++ b/frontend/src/metabase/setup/components/UserForm/UserForm.tsx
@@ -32,12 +32,14 @@ const USER_SCHEMA = Yup.object({
 
 interface UserFormProps {
   user?: UserInfo;
+  isHosted: boolean;
   onValidatePassword: (password: string) => Promise<string | undefined>;
   onSubmit: (user: UserInfo) => Promise<void>;
 }
 
 export const UserForm = ({
   user,
+  isHosted,
   onValidatePassword,
   onSubmit,
 }: UserFormProps) => {
@@ -66,7 +68,7 @@ export const UserForm = ({
             title={t`First name`}
             placeholder={t`Johnny`}
             nullable
-            autoFocus
+            autoFocus={!isHosted}
           />
           <FormInput
             name="last_name"
@@ -91,6 +93,10 @@ export const UserForm = ({
           type="password"
           title={t`Create a password`}
           placeholder={t`Shhh...`}
+          // Hosted instances always pass user information in the URLSearchParams
+          // during the initial setup. Password is the first empty field
+          // so it makes sense to focus on it.
+          autoFocus={isHosted && initialValues.site_name !== ""}
         />
         <FormInput
           name="password_confirm"
diff --git a/frontend/src/metabase/setup/components/UserStep/UserStep.tsx b/frontend/src/metabase/setup/components/UserStep/UserStep.tsx
index 8cf836f2554..b2d533e70ff 100644
--- a/frontend/src/metabase/setup/components/UserStep/UserStep.tsx
+++ b/frontend/src/metabase/setup/components/UserStep/UserStep.tsx
@@ -16,6 +16,7 @@ import { StepDescription } from "./UserStep.styled";
 
 export const UserStep = ({ stepLabel }: NumberedStepProps): JSX.Element => {
   const { isStepActive, isStepCompleted } = useStep("user_info");
+
   const user = useSelector(getUser);
   const isHosted = useSelector(getIsHosted);
 
@@ -45,6 +46,7 @@ export const UserStep = ({ stepLabel }: NumberedStepProps): JSX.Element => {
       )}
       <UserForm
         user={user}
+        isHosted={isHosted}
         onValidatePassword={validatePassword}
         onSubmit={handleSubmit}
       />
diff --git a/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx b/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx
index d41ee9a6300..6cff805c133 100644
--- a/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx
+++ b/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx
@@ -1,3 +1,4 @@
+import { mockSettings } from "__support__/settings";
 import { renderWithProviders, screen } from "__support__/ui";
 import type { SetupStep } from "metabase/setup/types";
 import type { UserInfo } from "metabase-types/store";
@@ -12,10 +13,16 @@ import { UserStep } from "./UserStep";
 interface SetupOpts {
   step?: SetupStep;
   user?: UserInfo;
+  isHosted?: boolean;
 }
 
-const setup = ({ step = "user_info", user }: SetupOpts = {}) => {
+const setup = ({
+  step = "user_info",
+  user,
+  isHosted = false,
+}: SetupOpts = {}) => {
   const state = createMockState({
+    settings: mockSettings({ "is-hosted?": isHosted }),
     setup: createMockSetupState({
       step,
       user,
@@ -32,6 +39,30 @@ describe("UserStep", () => {
     expect(screen.getByText("What should we call you?")).toBeInTheDocument();
   });
 
+  it("should autofocus the first name input field", () => {
+    setup({ step: "user_info" });
+
+    expect(screen.getByLabelText("First name")).toHaveFocus();
+  });
+
+  it("should autofocus the password input field for hosted instances", () => {
+    const user = createMockUserInfo();
+    setup({ step: "user_info", isHosted: true, user });
+
+    expect(screen.getByLabelText("Create a password")).toHaveFocus();
+  });
+
+  it("should pre-fill the user information if provided", () => {
+    const user = createMockUserInfo();
+    setup({ step: "user_info", user });
+
+    Object.values(user)
+      .filter(v => v.length > 0)
+      .forEach(v => {
+        expect(screen.getByDisplayValue(v)).toBeInTheDocument();
+      });
+  });
+
   it("should render in completed state", () => {
     setup({
       step: "db_connection",
diff --git a/frontend/src/metabase/setup/reducers.ts b/frontend/src/metabase/setup/reducers.ts
index dbfdec7c91c..469d50ec0e7 100644
--- a/frontend/src/metabase/setup/reducers.ts
+++ b/frontend/src/metabase/setup/reducers.ts
@@ -17,15 +17,33 @@ import {
   updateTracking,
 } from "./actions";
 
+const getUserFromQueryParams = () => {
+  const params = new URLSearchParams(window.location.search);
+  const getParam = (key: string, defaultValue = "") =>
+    params.get(key) || defaultValue;
+
+  return {
+    first_name: getParam("first_name") || null,
+    last_name: getParam("last_name") || null,
+    email: getParam("email"),
+    site_name: getParam("site_name"),
+    password: "",
+    password_confirm: "",
+  };
+};
+
 const initialState: SetupState = {
   step: "welcome",
   isLocaleLoaded: false,
   isTrackingAllowed: true,
+  user: getUserFromQueryParams(),
 };
 
 export const reducer = createReducer(initialState, builder => {
   builder.addCase(loadUserDefaults.fulfilled, (state, { payload: user }) => {
-    state.user = user;
+    if (user) {
+      state.user = user;
+    }
   });
   builder.addCase(
     loadLocaleDefaults.fulfilled,
-- 
GitLab