How do I redirect user to another page on Embedded App Admin Page?

How do I redirect user to another page on Embedded App Admin Page?

awidget
Shopify Partner
6 0 1

It's frustrating I can't make such simple thing as redirecting user to another app page after form submitting. 
I'm using recommended Remix cli template. I tried different approaches but they all fail.


Here is my app/routes/_index/route.jsx

 

import { useState } from "react";
import { useActionData } from "@remix-run/react";
import { authenticate } from "../../shopify.server";

import {
  AppProvider,
  Button,
  Text, ...
} from "@shopify/polaris";

export default function App() {
  ...
  const handleSubmit = async (event) => {
    event.preventDefault();
    setUiState('saving');
    // handling data ...

    here i need to redirect user to some page inside the app. let's say Test page.  How?
    //open('/app/test', '_self'); 
    //navigate('/app/test');
// const { redirect } = await authenticate.admin(request);
// return redirect('/app/test');

 

I'm getting different errors but none of the attempts was successful.

- with redirect()
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'admin')

- with open()
xxx.trycloudflare.com redirected you too many times.

- with const navigate = useNavigate();

Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

 

So what's the right method for this? Can you help please?

 

Replies 4 (4)

ncataland
Shopify Partner
2 0 3

Hi there. Shopify encourages using the Admin redirect helper for embedded apps. However, this helper must be called from within a Remix loader or action function. See below for usage examples. 

From the error output you provided, it looks like the admin context isn't loading correctly. Try changing the authenticate import path from "../../shopify.server" to "../shopify.server". Also, you might want to try moving your application logic from the app/routes/_index/route.jsx file to the app/routes/app._index.jsx file, as this is the main app landing page (though this may vary depending on the type of application you are developing.)

 

 

Usage examples

 

Use loader if you want the user to be redirected before page is loaded.

 

import { authenticate } from "../shopify.server";

export const loader = async ({ request }) => {
 	const { session, redirect } = await authenticate.admin(request);

	// App logic, if needed

 	return redirect("/app/other_page");
};

 

 

Use action if you want to collect data from the user, then redirect on submit.

 

import { Form, useActionData, useSubmit } from "@remix-run/react";
import { authenticate } from "../shopify.server";

export const action = async ({ request }) => {
 	const { session, redirect } = await authenticate.admin(request);

	// Await user form submission data
	const formData = await request.formData();
	
	// Do stuff with the data

 	return redirect("/app/other_page");
};

export default function Index() {

	const actionData = useActionData();
	const submit = useSubmit();
	const handleSave = () => submit(formData, { method: "POST" });

	return (
		<Form>
			<TextField value={userdata} label="Enter your data" type="text" name="userdata" />
			<Button variant="primary" onclick={handleSave} >Save</Button>
		</Form>
	)
}	

 

 

awidget
Shopify Partner
6 0 1

Thanks for you reply! But I still can't get it working. 

I used your code and updated it a little to avoid raising errors. 
However when I click the button nothing happens, no "start action" in console, no errors. What's wrong? Why action is not even triggered?

Here is my current code for this very basic page:

import { Form, redirect, useActionData, useSubmit } from "@remix-run/react";
import { authenticate } from "../../shopify.server";
import { useState } from "react";

import {
    AppProvider,
    TextField,
    Button,
  } from "@shopify/polaris";

export const action = async ({ request }) => {
  console.log("start action");

 	const { session, redirect } = await authenticate.admin(request);

	// Await user form submission data
	const formData = await request.formData();
	
	// Do stuff with the data

 	return redirect("/app/instructions");
};

export default function Index() {
  const [username, setUsername] = useState("");
	const formData = useActionData();
	const submit = useSubmit();
  const handleSave = () => submit(formData, { method: "POST" });

  const i18n = {
      Polaris: {
        ResourceList: {
        },
      },
    };
      
  const setUserdata = (value) => {
    console.log(value);
    setUsername(value);
  }

	return (
		<AppProvider i18n={i18n}>
        <Form  method="post">
            <TextField value={username} 
              onChange={setUserdata}
              label="Enter your data" type="text" name="username" />
            <Button variant="primary" onClick={handleSave} >Save</Button>
        </Form>
    </AppProvider>
	)
}	




BrainStation23
Shopify Partner
413 62 60

Hi @awidget ,

I see you're trying to submit a formData using the action. But the formData variable is out of scope for handleSave function. Since you're already using a state and a useSubmit to programmatically submit the form, I suggest instead of trying to use the formData, you use the username state itself. 

That way you can make sure that the post request has a body. By default, if you don't give any value or undefined as the request body, the remix will automatically redirect the request to the loader function instead of the action function. I've modified you're handleSave function code to run the action function as you submit the form.

 

 

  const handleSave = () => submit({ username }, { method: "POST" });

 


The result is community remix.PNG

 

Kazi Muktadir Ahmed | Brain Station 23
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution

Brain Station 23 PLC (Mail: js.sbu@brainstation-23.com)
- Was your question answered? Mark it as an Accepted Solution
- Did the solution not address your concern? We kindly request that share or mail your store URL with us this will enable us to collaborate more closely.
- Explore our Shopify public apps
zrrehmani
Shopify Partner
2 0 0

I am having an issue with redirects after i build my app using the npm run build command and putting it one the server 

below is the code 

import {
  Page,
  Text,
  BlockStack,
  Box,
  Button,
  InlineGrid,
  Card,
  FormLayout,
  TextField,
  Bleed,
  Grid,
  Frame,
  Toast,
  Icon,
  Spinner,
  InlineStack,
  Link,
} from "@shopify/polaris";
import {
  useSubmit,
  useNavigate,
  useActionData,
  redirect,
  useLoaderData,
} from "@remix-run/react";
import { authenticator } from "../services/auth.server";
import { useState, useEffect } from "react";
import { authenticate } from "../shopify.server";
import { ViewIcon, HideIcon, InfoIcon } from "@shopify/polaris-icons";
import {

  commitSession,
  getSession,
} from "../services/session.server";

export const loader = async ({ request }) => {
  const { redirect, session } = await authenticate.admin(request);
  let getsession = await getSession(request.headers.get("cookie"));
  const user = getsession.get(authenticator.sessionKey)
  if (user) {
    return redirect("/app/dashboard");
  }

  const forgot_password_url = process.env.SlEEK_NOTE_FORGOT_URL;
  const sleeknote_url = process.env.SlEEK_NOTE;
  return {
    forgot_password_url,
    sleeknote_url,
  };
};

export const action = async ({ request }) => {
  try {
    let user = await authenticator.authenticate("user-pass", request);
    let session = await getSession(request.headers.get("cookie"));
    session.set(authenticator.sessionKey, user);
    let headers = new Headers({
      "Set-Cookie": await commitSession(session),
    });

    if (user.status == 0) {
      return redirect("/app/dashboard", { headers });
    }
  } catch (error) {
    return error;
  }
};

export default function Index() {
  const navigate = useNavigate();
  const submit = useSubmit();
  const laderData = useLoaderData();
  const actionData = useActionData();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [errors, setErrors] = useState({ email: "", password: "" });
  const [toastMessage, setToastMessage] = useState(null);
  const [errorToastMessage, setErrorToastMessage] = useState(null);
  const [passwordVisible, setPasswordVisible] = useState(false);
  const [isPasswordFocused, setIsPasswordFocused] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  console.log(laderData);
  useEffect(() => {
    if (actionData?.message) {
      setErrorToastMessage(actionData?.message);
      setIsLoading(false);
    }
  }, [actionData]);

  const sleeknoteBtn = () => {
    window.open(laderData.sleeknote_url);
  };

  const validateForm = () => {
    let isValid = true;
    let errors = { email: "", password: "" };

    if (!email.trim()) {
      errors.email = "Email is required";
      isValid = false;
    }

    if (!password) {
      errors.password = "Password is required";
      isValid = false;
    } else if (password.length < 6) {
      errors.password = "Password must be at least 6 characters long";
      isValid = false;
    }

    setErrors(errors);
    return isValid;
  };

  const onLogin = () => {
    if (validateForm()) {
      setIsLoading(true);
      try {
        submit({ email, password }, { method: "POST" });
      } catch (error) {
        setIsLoading(false);
      }
    }
  };

  const forgotPassword = () => {
    window.open(laderData.forgot_password_url);
  };
  return (
    <Frame>
      {toastMessage && (
        <Toast content={toastMessage} onDismiss={() => setToastMessage(null)} />
      )}
      {errorToastMessage && (
        <Toast
          error
          content={errorToastMessage}
          onDismiss={() => setErrorToastMessage(null)}
        />
      )}
      <Page>
        <Grid>
          <Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 4, lg: 8, xl: 7 }}>
            <Box paddingBlock="1200">
              <BlockStack gap="300">
                <Text variant="headingXl" as="h4">
                  Engage your visitors with on-site messages
                </Text>
                <Text variant="bodyLg" as="p">
                  Try Sleeknote for free and set your first campaign live on
                  your Shopify store.
                </Text>
              </BlockStack>
            </Box>
          </Grid.Cell>
          <Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 2, lg: 4, xl: 3 }}>
            <Box>
              <img
                src="/envelop.svg"
                alt="envelope"
                width={240.89}
                height={150}
              />
            </Box>
          </Grid.Cell>
        </Grid>

        <Grid>
          <Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 2, lg: 4, xl: 4 }}>
            <Box paddingBlock="1200">
              <BlockStack gap="300">
                <Text as="p" fontWeight="bold" variant="headingMd">
                  Get started
                </Text>
                <Text>
                  Try Sleeknote for free and set your first campaign live on
                  your Shopify store.
                </Text>
              </BlockStack>
            </Box>
          </Grid.Cell>
          <Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 4, lg: 8, xl: 6 }}>
            <Box paddingBlock="1200">
              <Card roundedAbove="sm">
                <InlineGrid columns="1fr auto">
                  <Text>
                    Start a 14-days free trial access all our features without
                    spending a cent (And return to this page once you're done)
                  </Text>
                  <BlockStack align="center">
                    <Button
                      onClick={() => {
                        navigate("/app/Register");
                      }}
                      variant="primary"
                    >
                      Start free trial
                    </Button>
                  </BlockStack>
                </InlineGrid>
              </Card>
            </Box>
          </Grid.Cell>
        </Grid>

        <Grid>
          <Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 2, lg: 4, xl: 4 }}>
            <Box paddingBlock="1200">
              <BlockStack gap="300">
                <Text as="p" fontWeight="bold" variant="headingMd">
                  Already have an account?
                </Text>
                <Text>
                  Log in with the email address and password you use for your
                  Sleeknote account.
                </Text>
              </BlockStack>
            </Box>
          </Grid.Cell>
          <Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 4, lg: 8, xl: 6 }}>
            <Box paddingBlock="1200">
              <Card roundedAbove="sm">
                <FormLayout>
                  <TextField
                    label="Username"
                    autoComplete="off"
                    name="email"
                    value={email}
                    onChange={(e) => setEmail(e)}
                  />
                  {errors?.email && (
                    <span style={{ color: "red" }}>{errors.email}</span>
                  )}
                  <Box>
                    <BlockStack inlineAlign="end">
                      <Bleed marginBlockEnd="500">
                        <Button variant="plain" onClick={forgotPassword}>
                          Forgot password?
                        </Button>
                      </Bleed>
                    </BlockStack>
                    <TextField
                      type={passwordVisible ? "text" : "password"}
                      label="Password"
                      autoComplete="current-password"
                      name="password"
                      value={password}
                      onChange={(e) => setPassword(e)}
                      onFocus={() => setIsPasswordFocused(true)}
                      onBlur={() => setIsPasswordFocused(false)}
                      suffix={
                        isPasswordFocused && (
                          <Button
                            variant="plan"
                            onClick={() => setPasswordVisible(!passwordVisible)}
                          >
                            {passwordVisible ? (
                              <Icon source={HideIcon} tone="base" />
                            ) : (
                              <Icon source={ViewIcon} tone="base" />
                            )}
                          </Button>
                        )
                      }
                    />
                    {errors?.password && (
                      <span style={{ color: "red" }}>{errors.password}</span>
                    )}
                  </Box>
                  <BlockStack inlineAlign="end">
                    <Button onClick={onLogin} disabled={isLoading}>
                      {isLoading ? (
                        <Spinner accessibilityLabel="Loading" size="small" />
                      ) : (
                        "Login"
                      )}
                    </Button>
                  </BlockStack>
                </FormLayout>
              </Card>
            </Box>
          </Grid.Cell>
        </Grid>

        <Grid>
          <Grid.Cell columnSpan={{ xs: 6, sm: 6, md: 12, lg: 10, xl: 11 }}>
            <BlockStack inlineAlign="center">
              <Text>
                <InlineStack wrap="false" gap="100">
                  <Icon source={InfoIcon} tone="magic" />
                  Learn more about
                  <Button variant="plain" onClick={sleeknoteBtn}>
                    Sleeknote
                  </Button>
                </InlineStack>
              </Text>
            </BlockStack>
          </Grid.Cell>
        </Grid>
      </Page>
    </Frame>
  );
}
 
in the loader function if the user is true then it will ask for the shop password and when I add the password then it will say there was a problem