Build a Paid Membership Site with Magic and Stripe: Pt. 2 - React Client
Tags: passwordless, stripe, magic, auth
Published: 2021-03-10 01:01:25

---
title: Build a Paid Membership Site with Magic and Stripe: Pt. 2 - React Client
published: true
description: A deep dive into how we built the client side of our paid membership website.
tags: passwordless, stripe, magic, auth
cover_image: https://i.imgur.com/EElejSN.png
---

This is the second part of the Build a Paid Membership Site with Magic and Stripe series. Make sure to follow our [**Quickstart article**](https://dev.to/magiclabs/build-a-paid-membership-site-with-magic-and-stripe-pt-1-quickstart-bp) before proceeding.

# Client
Let's dive right in by going over the major steps we need to follow to build the paid membership site's Client side:
1. Set up the user sign up, payment, login, and logout flows.
2. Build the payment form as well as the Payment page that will house the form.
3. Make this Payment page accessible to the user by creating a Payment Route.

## Standard Auth Setup
### Keep Track of the Logged In User
We'll be keeping track of the logged in user's state with React's `useContext` hook. Inside `App.js`, wrap the entire app in `<UserContext.Provider>`. This way, all of the child components will have access to the hook we created (namely, `const [user, setUser] = useState();`) to help us determine whether or not the  user is logged in. 

```js
/* File: client/src/App.js */

import React, { useState, useEffect } from "react";
import { Switch, Route, BrowserRouter as Router } from "react-router-dom";
import { UserContext } from "./lib/UserContext";

// Import UI components
import Home from "./components/home";
import  PremiumContent  from  "./components/premium-content";
import Login from "./components/login";
import  SignUp  from  "./components/signup";
import Profile from "./components/profile";
import Layout from "./components/layout";

// Import Magic-related things
import { magic } from "./lib/magic";

function App() {

  // Create a hook to help us determine whether or not the  user is logged in
  const [user, setUser] = useState();

  // If isLoggedIn is true, set the UserContext with user data
  // Otherwise, set it to {user: null}
  useEffect(() => {
    setUser({ loading: true });
    magic.user.isLoggedIn().then((isLoggedIn) => {
      return isLoggedIn
        ? magic.user.getMetadata().then((userData) => setUser(userData))
        : setUser({ user: null });
    });
  }, []);

  return (
    <Router>
      <Switch>
        <UserContext.Provider value={[user, setUser]}>
              <Layout>
                <Route path="/" exact component={Home} />
                <Route path="/premium-content" component={PremiumContent} />
                <Route path="/signup" component={SignUp} />
                <Route path="/login" component={Login} />
                <Route path="/profile" component={Profile} />
              </Layout>
        </UserContext.Provider>
      </Switch>
    </Router>
  );
}

export default App;
```
**Note**: Once a user logs in with Magic, unless they log out, they'll remain authenticated for 7 days.

### Keep Track of the Paid User
We'll also be keeping track of whether or not the user has paid for lifetime access with the `useContext` hook. Again, inside of `App.js`, we wrap the entire app with two new contexts: `<LifetimeContext>`, then `<LifetimeAccessRequestStatusContext>`.

```js
/* File: client/src/App.js */
import React, { useState, useEffect } from "react";
import { Switch, Route, BrowserRouter as Router } from "react-router-dom";
import { UserContext } from "./lib/UserContext";
import { LifetimeContext } from "./lib/LifetimeContext";
import { LifetimeAccessRequestStatusContext } from "./lib/LifetimeAccessRequestStatusContext";

// Import UI components
import Home from "./components/home";
import  PremiumContent  from  "./components/premium-content";
import Login from "./components/login";
import  SignUp  from  "./components/signup";
import Profile from "./components/profile";
import Layout from "./components/layout";

// Import Magic-related things
import { magic } from "./lib/magic";

function App() {

  // Create a hook to check whether or not user has lifetime access
  const [lifetimeAccess, setLifetimeAccess] = useState(false);
  // Create a hook to prevent infinite loop in useEffect inside of /components/premium-content
  const [
    lifetimeAccessRequestStatus,
    setLifetimeAccessRequestStatus,
  ] = useState("");
  // Create a hook to help us determine whether or not the  user is logged in
  const [user, setUser] = useState();

  // If isLoggedIn is true, set the UserContext with user data
  // Otherwise, set it to {user: null}
  useEffect(() => {
    setUser({ loading: true });
    magic.user.isLoggedIn().then((isLoggedIn) => {
      return isLoggedIn
        ? magic.user.getMetadata().then((userData) => setUser(userData))
        : setUser({ user: null });
    });
  }, []);

  return (
    <Router>
      <Switch>
        <UserContext.Provider value={[user, setUser]}>
          <LifetimeContext.Provider value={[lifetimeAccess, setLifetimeAccess]}>
            <LifetimeAccessRequestStatusContext.Provider
              value={[
                lifetimeAccessRequestStatus,
                setLifetimeAccessRequestStatus,
              ]}
            >
              <Layout>
                <Route path="/" exact component={Home} />
                <Route path="/premium-content" component={PremiumContent} />
                <Route path="/signup" component={SignUp} />
                <Route path="/login" component={Login} />
                <Route path="/profile" component={Profile} />
              </Layout>
            </LifetimeAccessRequestStatusContext.Provider>
          </LifetimeContext.Provider>
        </UserContext.Provider>
      </Switch>
    </Router>
  );
}

export default App;
```

As you can see, we've added two new hooks. The first hook will help us determine whether or not user has lifetime access:

```js
const [lifetimeAccess, setLifetimeAccess] = useState(false);
```

While the second hook will help us prevent [an infinite loop of component re-renderings](https://dmitripavlutin.com/react-useeffect-infinite-loop/#:~:text=The%20state%20update%20triggers%20re,and%20so%20on%20indefinitely.) caused by the `useEffect` inside of `/components/premium-content`.

```js
  const [
    lifetimeAccessRequestStatus,
    setLifetimeAccessRequestStatus,
  ] = useState("");
```

### Log in with Magic Link Auth
In `client/src/components/login.js`, `magic.auth.loginWithMagicLink()` is what triggers the magic link to be emailed to the user. It takes an object with two parameters, `email` and an optional `redirectURI`. 

Magic allows you to configure the email link to open up a new tab, bringing the user back to your application. Since we won't be using redirect, the user will only get logged in on the original tab. 

Once the user clicks the email link, we send the [`didToken`](https://docs.magic.link/decentralized-id) to a server endpoint at `/login` to validate it. If the token is valid, we update the user's state by setting the `UserContext` and then redirect them to the profile page.

```js
/* File: client/src/components/login.js */

  async function handleLoginWithEmail(email) {
    try {
      setDisabled(true); // Disable login button to prevent multiple emails from being triggered

      // Trigger Magic link to be sent to user
      let didToken = await magic.auth.loginWithMagicLink({
        email,
      });

      // Validate didToken with server
      const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/login`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer " + didToken,
        },
      });

      if (res.status === 200) {
        // Get info for the logged in user
        let userMetadata = await magic.user.getMetadata();
        // Set the UserContext to the now logged in user
        await setUser(userMetadata);
        history.push("/profile");
      }
    } catch (error) {
      setDisabled(false); // Re-enable login button - user may have requested to edit their email
      console.log(error);
    }
  }
```
### Sign up with Magic Link Auth
We'll be applying practically the same code as in the `Login` component to our `SignUp` component (located in `client/src/components/signup.js`). The only difference is the user experience. 

When a user first lands on our page, they'll have access to our Free Content. 
![Free Content](https://i.imgur.com/CWElKlY.png)
We can also show them a sneak peek of our awesomeness in the Premium Content page.
![Premium Content](https://i.imgur.com/fDy0sCo.png)
Once they realize how awesome we are, and have decided to pay $500 for a lifetime access pass, they can click **Count Me In**.
![Sign Up](https://i.imgur.com/QyxJvY1.png)
Since the user is not logged in, our app will ask them to first sign up for a new account. Once they've been authenticated by Magic, they'll be redirected to the Payment page where they can seal the deal to a lifetime access of awesomeness!

### Log out with Magic
To allow users to log out, we'll add a `logout` function in our `Header` component. `logout()` ends the user's session with Magic, clears the user's information from the UserContext, resets both the user's lifetime access as well as the lifetime access request status, and redirects the user back to the login page.

```js
/* File: client/src/components/header.js */

const logout = () => {
  magic.user.logout().then(() => {
    setUser({ user: null }); // Clear user's info
    setLifetimeAccess(false); // Reset user's lifetime access state
    setLifetimeAccessRequestStatus(""); // Reset status of lifetime access request
    history.push('/login');
  });
};
```

## Build the Payment Form
This is where we'll build out the `PaymentForm` component that's located in `client/src/components/payment-form.js`.

### Set up the State
To create the `PaymentForm` component, we'll first need to initialize some state to keep track of the payment, show errors, and manage the user interface.
```js
/* File: client/src/components/payment-form.js */

  const [succeeded, setSucceeded] = useState(false);
  const [error, setError] = useState(null);
  const [processing, setProcessing] = useState("");
  const [disabled, setDisabled] = useState(true);
  const [clientSecret, setClientSecret] = useState("");
```
There are two more states we need. One to keep track of the customer we create:
```js
/* File: client/src/components/payment-form.js */

  const [customerID, setCustomerID] = useState("");
```
And the other to set the lifetime access state to true if the user's payment was successful:
```js
/* File: client/src/components/payment-form.js */

  const [, setLifetimeAccess] = useContext(LifetimeContext);
```

### Store a Reference to Stripe
Since we're using Stripe to process the Customer's payment, we'll need to access the Stripe library. We do this by calling Stripe's `useStripe()` and `useElements()` hooks.
```js
/* File: client/src/components/payment-form.js */

  const stripe = useStripe();
  const elements = useElements();
```

### Fetch a PaymentIntent
As soon as the `PaymentForm` loads,  we'll be making a request to the `/create-payment-intent` endpoint in our `server.js` file. Calling this route will create a Stripe Customer as well as a Stripe [`PaymentIntent`](https://stripe.com/docs/api/payment_intents). `PaymentIntent` will help us keep track of the Customer's payment cycle.

The `data` that the Client gets back includes the `clientSecret` returned by `PaymentIntent`. We'll be using this to complete the payment, so we've saved it using `setClientSecret()`. The `data` also includes the ID of the Customer that the `PaymentIntent` belongs to. We'll need this ID when we update the Customer's information, so we’ll also be saving it with `setCustomerID()`.

```js
/* File: client/src/components/payment-form.js */

  useEffect(() => {
    // Create PaymentIntent as soon as the page loads
    fetch(`${process.env.REACT_APP_SERVER_URL}/create-payment-intent`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
    })
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        setClientSecret(data.clientSecret);
        setCustomerID(data.customer);
      });
  }, [email]);
```

### Update the Stripe Customer
If the Stripe payment transaction was successful, we'll send the Customer's ID to our server's `/update-customer` endpoint to update the Stripe Customer's information so that it includes a metadata which will help us determine whether or not the user has lifetime access. 

Once this request has completed, we can finally redirect the Customer to the Premium Content page and let them bask in the awesomeness of our content.

```js
/* File: client/src/components/payment-form.js */

  const handleSubmit = async (ev) => {
    ev.preventDefault();
    setProcessing(true);
    const payload = await stripe.confirmCardPayment(clientSecret, {
      payment_method: {
        card: elements.getElement(CardElement),
      },
    });

    if (payload.error) {
      setError(`Payment failed ${payload.error.message}`);
      setProcessing(false);
    } else {
      setError(null);
      setProcessing(false);
      setSucceeded(true);
      setLifetimeAccess(true);
      // Update Stripe customer info to include metadata
      // which will help us determine whether or not they
      // are a Lifetime Access member.
      fetch(`${process.env.REACT_APP_SERVER_URL}/update-customer`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ customerID }),
      })
        .then((res) => {
          return res.json();
        })
        .then((data) => {
          console.log("Updated Stripe customer object: ", data);
          history.push("/premium-content");
        });
    }
  };
```

### Add a CardElement
One last task to complete the `PaymentForm` component is to add the `CardElement` component provided by Stripe. The `CardElement` embeds an iframe with the necessary input fields to collect the card data. This creates a single input that collects the card number, expiry date, CVC, and postal code.

```js
/* File: client/src/components/payment-form.js */

        <CardElement
          id="card-element"
          options={cardStyle}
          onChange={handleChange}
        />
```

## Build the Payment Page
Now that our `PaymentForm` component is ready, it's time to build the `Payment` component which will house it! This component is located in `client/src/components/payment.js`.

The two most important points to note about the `Payment` component are:
1. We'll be using the `user` state that we set in `UserContext` to check whether or not the user is logged in. 
2. The component will take in `Elements`, `PaymentForm`, and `promise` as props to help us properly render the Stripe payment form.

```js
/* File: client/src/components/payment.js */

import { useContext, useEffect } from "react";
import { useHistory } from "react-router";
import { UserContext } from "../lib/UserContext";
import Loading from "./loading";

export default function Payment({ Elements, PaymentForm, promise }) {
  const [user] = useContext(UserContext);
  const history = useHistory();

  // If not loading and no user found, redirect to /login
  useEffect(() => {
    user && !user.loading && !user.issuer && history.push("/login");
  }, [user, history]);

  return (
    <>
      <h3 className="h3-header">
        Purchase Lifetime Access Pass to Awesomeness 🤩
      </h3>
      <p>
        Hi again {user?.loading ? <Loading /> : user?.email}! You successfully
        signed up with your email. Please enter your card information below to
        purchase your Lifetime Access Pass securely via Stripe:
      </p>
      {user?.loading ? (
        <Loading />
      ) : (
        <Elements stripe={promise}>
          <PaymentForm email={user.email} />
        </Elements>
      )}
      <style>{`
        p {
          margin-bottom: 15px;
        }
        .h3-header {
          font-size: 22px;
          margin: 25px 0;
        }
      `}</style>
    </>
  );
}
```

## Add the Payment Route to App.js
Alright, with the `PaymentForm` and `Payment` components complete, we can finally route the user to the `/payment` page by updating `client/src/App.js`! 

First, we import **Stripe.js** and the **Stripe Elements UI library** into our `App.js` file:

```js
/* File: client/src/App.js */

import { loadStripe } from  "@stripe/stripe-js";
import { Elements } from  "@stripe/react-stripe-js";
```

Then we'll load `Stripe.js` **outside** of the `App.js`'s render to avoid recreating the Stripe object on every render:

```js
/* File: client/src/App.js */

const  promise = loadStripe(process.env.REACT_APP_STRIPE_PK_KEY);
```

**Note**: As we saw in [Build the Payment Page](http://localhost:3000/posts/magic-stripe#build-the-payment-page), `promise` is a prop that is passed into the `Payment` component and is used by the `Elements` provider to give it's child element, `PaymentForm`, access to the Stripe service.

Next, let's add a new route called `/payment` which returns the `Payment` component we created earlier with the props required to properly render the Stripe payment form.

```js
/* File: client/src/App.js */

function  App() {

...
                <Route
                  path="/payment"
                  render={(props) => {
                    return (
                      <Payment
                        Elements={Elements}
                        PaymentForm={PaymentForm}
                        promise={promise}
                      />
                    );
                  }}
                />
 ...               
}
export  default  App;
```

# What's next
Now that you know how the sign up, payment, login, and logout pages were built and how they work, it's time to learn how the Node server powers the paid membership site. Click [**here**](https://dev.to/magiclabs/build-a-paid-membership-site-with-magic-and-stripe-pt-3-node-server-1f73) to continue.