Skip to content

Latest commit

 

History

History
138 lines (103 loc) · 8.55 KB

logginging.md

File metadata and controls

138 lines (103 loc) · 8.55 KB

Logging in

Create API

There are different ways that you could set up the login flow for connecting to an Urbit ship through JS. Notice that in this example app we are keeping all the code in one file, this is to serve as a reference more than recommended architecture for a functional app. With that in mind, we'll be using a very simple method to allow a user to login to their ship and store their credentials in localStorage for ease. It starts with a function we'll call createApi outside of our main App() function on line 32.

const createApi = (host: string, code: string) =>
  _.memoize(
    (): UrbitInterface => {
      const urb = new Urbit(host, code);
      urb.ship = "zod";
      urb.onError = (message) => console.log(message);
      urb.connect();
      return urb;
    }
  );

While this tutorial does its best to be self-contained, notice here that we are using the memoize method courtesy of lodash which we import at the top of App.tsx. This essentially caches the result of the function to reduce the amount of expensive computation our app has to do.

Next thing to notice is that we're using two imports from @urbit/http-api. UrbitInterface is is a TypeScript interface that lives in @urbit/http-api/dist/types.d.s Read it's source to see the methods and variables it contains. We are passing it a variable called urb that will hold an Urbit object (which lives in @urbit/http-api/dist/Urbit.d.ts).

The Urbit object itself accepts a host url and a ship code. The host is the port on localhost that our Urbit uses to interact with the web, and the code is the authentication code that we can use to open this connection. Once we pass in the urb object we can now call methods on it such as .connect() which establishes our connection to our ship.

Refer to the rest of UrbitInterface to see other calls we will make in this tutorial, especially poke thread scry and subscribe, these are the four ways we will interact with our ship.

Take note that after we connect to our urb we then have the createApi() function return the urb object itself.

Determining Whether a User Has Already Logged in

Refer to the breakout lesson on React Hooks for a more detailed explanation of useState and useEffect, for the purposes of logging in we'll just say that these two hooks allow us to check to see whether or not a user has previously logged in, and if so, to keep track of their login status with a state variable that is accessible to our entire app.

The first state variable we define is loggedIn on line 46:

const [loggedIn, setLoggedIn] = useState<boolean>();

We defined the variable name as loggedIn and useState then gives us a function for free to update it. Here we're calling it setLoggedIn. Notice how these two names are destructured from the useState hook which we are defining as a boolean.

Next we define the second state vaiable urb on line 47:

const [urb, setUrb] = useState<UrbitInterface | undefined>(); // Stores our Urbit connection. Notice we declare the type as UrbitInterface

Same as loggedIn, urb is a variable that will store our urb object and we get a function setUrb to add it to our state so that anywhere in our app we can call functions directly on our urb.

Then on line 54 we have a function to setloggedIn and tell our app whether or not our user has already logged in:

  useEffect(() => {
    if (localStorage.getItem("host") && localStorage.getItem("code")) {
      setLoggedIn(true);
      const _urb = createApi(
        localStorage.getItem("host")!,
        localStorage.getItem("code")!
      );
      setUrb(_urb);
      return () => {};
    }
  }, []);

The short explanation of useEffect is that it replaces the traditional React lifecycle and allows us to specify actions to perform after the intial render is complete. Here we're checking to see if host and code exist in localStorage. If so then we use our setLoggedIn function described above to set the loggedIn state variable to true. Now we can run our createApi() function from earlier, passing it the variables from localStorage.

Remember that createApi() returns an urb object. Here we call it from within the variable _urb which will become our connected urb once createApi() finishes. We then run setUrb(_urb) (our useState function that stores urb) and thus our entire app now has access to our ship object. In the future we'll use this object a lot to call functions such as urb.poke() urb.thread() etc.

Notice the empty array [] at the end of the function. useEffect allows us to give it variables or functions to monitor, and if they change the effect will run again with the new data. Since we only want this effect to run after the initial render we just pass an empty array [] so it only runs once.

Login Flow

The previous section covered situations in which a user has already used our app to login. If they have not, then the useEffect() function will skip over the connect function. Now let's look at how a new user can log in. The next function we see on line 68 is

  const login = (host: string, code: string) => {
    localStorage.setItem("host", host);
    localStorage.setItem("code", code);
    const _urb = createApi(host, code);
    setUrb(_urb);
    setLoggedIn(true);
    return () => {};
  };

In a moment we'll see the UI that calls this function. For now note that we take a host and code string (rember that an Urbit object uses these two parameters to log in) and add them to localStorage. Then using the same format from the last function we looked at, we call our createApi() function, passing it the host and code credentials and then storing the resulting object in our state as urb.

This time as well we set the loggedIn state variable to true. You should now be familiar with the pattern of using the functions that useState gives us to modify state variables. With this, we are connected to our ship, our whole app now has access to our ship to call functions, and our whole app knows that we are logged in. This will come in handy when we look at how we collect the login credentials from our user below.

UI

This section starts on line 413. The <form> tag is mostly boiler plate React TypeScript code for collecting and submitting <input> data. We're just adding a target object that has a host and code key and handles the input from our text fields. We destructure each of those keys into its own variable, and then passing them into the login() function that we explained above. We will use some variation of this pattern for each of our input fields in this app.

<pre>Login:</pre>
  <form
    onSubmit={(e: React.SyntheticEvent) => {
      e.preventDefault();
      const target = e.target as typeof e.target & {
        host: { value: string };
        code: { value: string };
      };
      const host = target.host.value;
      const code = target.code.value;
      login(host, code);
    }}
  >

Now we will use our loggedIn state variable with a ternary operator to determine whether we should render UI for a user who has already logged in, or UI asking the user to login. We see this in the placeholder prop.

loggedIn ? localStorage.getItem("host")! : "Host"

Simply writing loggedIn actually means "if loggedIn is true." (You would write !loggedIn if you wanted "if loggedIn is false). The ? is the equivalent of then and here we're having it render host from localStorage. As in, if the user is logged in then use host from localStorage as the placeholder in our text field. The ! at the end of this line is to tell TypeScript that we know host will be a string.

The : here serves as an else statement. If we don't already know the user's host then this placeholder serves as a prompt for them to enter it.

Here is the rest of the code snippet. Notice we're doing the same thing for code. Again this is a very simple application of conditional rendering, you can use ternary operators to completely customize your UI for a user who is logged in versus one who isn't.

  {/* We are using ternary operators to get if the use already has login info in localStorage. If so we render that info as a placeholder
  for each input form. Otherwise we render 'Host' or 'Code' as the placeholder*/}
  <input
    type="host"
    name="host"
    placeholder={
      loggedIn ? localStorage.getItem("host")! : "Host"
    }
  />
  <br />
  <input
    type="code"
    name="code"
    placeholder={
      loggedIn ? localStorage.getItem("code")! : "Code"
    }
  />
  <br />
  <input type="submit" value="Login" />
</form>