Stripe, React and Serverless — Part 2

Stripe, React and Serverless — Part 2

Stripe, React and Serverless — Part 2

In this article series, we will talk about how we integrated Sigmetic with Stripe using React and Serverless.

In part 1, we implemented the back-end using Serverless. Here in part 2, we will integrate the front-end in React using Stripe Elements.

Prerequisites

React application

We assume that you have a front-end in React and that this is where you will have your customers subscribe to your service. One of the cool things about Stripe is, that we can collect payment information natively in the application instead of redirecting to a third-party or embedding iframe forms that look entirely different from the rest of your UI.

We will be using Stripe Elements for this. For Sigmetic, we have a page for upgrading that looks like this

With these 3 things in place, let’s get started 🔥

Stripe Manager

To make your life easier, we suggest that you write a set of utility functions that you can use to make requests to the cloud functions that we implemented in part 1.

For Sigmetic, we created a class StripeManager with a set of static methods. In this way, you can access the methods like StripeManager.createSubscription(); throughout the application.

You can also export the individual functions and import them like: import * as StripeManager from '../services/StripeManager' if you like that pattern better.

For this article, we are going with the class.

So, create a new file somewhere appropriate, like src/services/StripeManager.ts, and create a new class definition.

class StripeManager {

}

export default StripeManager;

Database

We assume that you have a database with users. Now is the time to extend your user model to include 3 new fields:

  • customerID

  • paymentMethodID

  • subscriptionID

  • hasPaidPlan

For Sigmetic, we use GraphQL and DynamoDB.

Read more about GRADS: The Tech Stack of Sigmetic

We will mention when to write / read from the database, but we will not go into further details about the implementation in this article.

Create new customer

First, let’s create an interface representing a Stripe Customer. Create a new file src/interfaces/IStripeCustomer:

export interface IStripeCustomer {
  id: string;
  object: string;
  address?: any;
  balance: number;
  created: number;
  currency: string;
  default_source?: any;
  delinquent: boolean;
  description: string;
  discount?: any;
  email?: any;
  invoice_prefix: string;
  invoice_settings: IStripeInvoiceSettings;
  livemode: boolean;
  metadata: any;
  name?: any;
  next_invoice_sequence: number;
  phone?: any;
  preferred_locales: any[];
  shipping?: any;
  sources: IStripeSources;
  subscriptions: IStripeSubscriptions;
  tax_exempt: string;
  tax_ids: IStripeTaxIds;
}

export interface IStripeInvoiceSettings {
  custom_fields?: any;
  default_payment_method?: any;
  footer?: any;
}

export interface IStripeSources {
  object: string;
  data: any[];
  has_more: boolean;
  url: string;
}

export interface IStripeSubscriptions {
  object: string;
  data: any[];
  has_more: boolean;
  url: string;
}

export interface IStripeTaxIds {
  object: string;
  data: any[];
  has_more: boolean;
  url: string;
}

💡 Hint: Use json2ts to quickly create TypeScript interfaces from JSON.

Now, let’s create a method for creating a new customer

class StripeManager {
  public static async createCustomer() {
    try {
      // Retrieve email and username of the currently logged in user.
      // getUserFromDB() is *your* implemention of getting user info from the DB
      const { email, username } = getUserFromDB();

      if (!email || !username) {
        throw Error('Email or username not found.');
      }

      const request = await fetch('https://your-endpoint/stripe/create-customer', {
        method: 'POST',
        body: JSON.stringify({
          email,
          username,
        }),
      });

      const customer = (await request.json()) as IStripeCustomer;

      // Update your user in DB to store the customerID
      // updateUserInDB() is *your* implementation of updating a user in the DB
      updateUserInDB({ customerID: customer.id });

      return customer;
    } catch (error) {
      console.log('Failed to create customer');
      console.log(error);
      return null;
    }
  }
}

Now that we’re at it, let’s create a method for retrieving the customerID, and — if none exists — create a new one using the above method.

class StripeManager {
  ...

  public static async getStripeCustomerID() {
    // Retrieve the current customerID from the currently logged in user
    // getUserFromDB() is *your* implemention of getting user info from the DB
    const { customerID } = getUserFromDB();

    if (!customerID) {
      const customer = await this.createCustomer();
      return customer?.id;
    }

    return customerID;
  }

  ...
}

Create subscription

Again, let’s create a new interface for a Stripe Subscription: Create a new file src/interfaces/IStripeSubscription:

export interface IStripeSubscription {
  id: string;
  object: string;
  application_fee_percent?: any;
  billing_cycle_anchor: number;
  billing_thresholds?: any;
  cancel_at?: any;
  cancel_at_period_end: boolean;
  canceled_at?: any;
  collection_method: string;
  created: number;
  current_period_end: number;
  current_period_start: number;
  customer: string;
  days_until_due?: any;
  default_payment_method?: any;
  default_source?: any;
  default_tax_rates: any[];
  discount?: any;
  ended_at?: any;
  items: IStripeItems;
  latest_invoice?: any;
  livemode: boolean;
  metadata: any;
  next_pending_invoice_item_invoice?: any;
  pause_collection?: any;
  pending_invoice_item_interval?: any;
  pending_setup_intent?: any;
  pending_update?: any;
  quantity: number;
  schedule?: any;
  start_date: number;
  status: string;
  tax_percent?: any;
  trial_end?: any;
  trial_start?: any;
}

export interface IStripeRecurring {
  aggregate_usage?: any;
  interval: string;
  interval_count: number;
  trial_period_days: number;
  usage_type: string;
}

export interface IStripePrice {
  id: string;
  object: string;
  active: boolean;
  billing_scheme: string;
  created: number;
  currency: string;
  livemode: boolean;
  lookup_key?: any;
  metadata: any;
  nickname: string;
  product: string;
  recurring: IStripeRecurring;
  tiers?: any;
  tiers_mode?: any;
  transform_quantity?: any;
  type: string;
  unit_amount: number;
  unit_amount_decimal: string;
}

export interface IStripeDatum {
  id: string;
  object: string;
  billing_thresholds?: any;
  created: number;
  metadata: any;
  price: IStripePrice;
  quantity: number;
  subscription: string;
  tax_rates: any[];
}

export interface IStripeItems {
  object: string;
  data: IStripeDatum[];
  has_more: boolean;
  url: string;
}

Then, let’s create a method for creating a new subscription:

class StripeManager {
  ...

  public static async createSubscription(customerID: string, paymentMethodID: string) {
    const request = await fetch('https://your-endpoint/stripe/create-subscription', {
      method: 'POST',
      body: JSON.stringify({
        customerID,
        paymentMethodID,
      }),
    });

    const subscription = await request.json() as IStripeSubscription;
    if (subscription.status !== 'active') {
      throw Error('Unable to upgrade. Please try again');
    }

    if (subscription.latest_invoice.payment_intent.status === 'requires_payment_method') {
      throw Error('You credit card was declined. Please try again with another card.');
    }

    // Update your user in DB to store the subscriptionID and enable paid plan
    // updateUserInDB() is *your* implementation of updating a user in the DB
    updateUserInDB({
      paymentMethodID,
      hasPaidPlan: true,
      subscriptionID: subscription.id,
    });

    return subscription;
  }

  ...
}

While we’re at it — let’s add a method for updating the subscription as well.

class StripeManager {
  ...

  public static async handleSubscription(subscriptionID: string, end: boolean) {

    const request = await fetch('https://your-endpoint/stripe/handle-subscription', {
      method: 'POST',
      body: JSON.stringify({
        subscriptionID,
        end,
      }),
    });

    return await request.json() as IStripeSubscription;
  }

  ...
}

And finally, one for retrieving the subscription

class StripeManager {
  ...

  public static async retrieveSubscription(subscriptionID: string) {
    const request = await fetch('https://your-endpoint/stripe/retrieve-subscription', {
      method: 'POST',
      body: JSON.stringify({
        subscriptionID,
      }),
    });

    return await request.json() as IStripeSubscription;
  }

  ...
}

Update payment method

Alright, now we just need a couple of more methods.

Let’s add one for updating the payment method:

class StripeManager {
  ...

  public static async updatePaymentMethod(customerID: string, paymentMethodID: string) {
    await fetch('https://your-endpoint/stripe/update-payment-method', {
      method: 'POST',
      body: JSON.stringify({
        customerID,
        paymentMethodID,
      }),
    });

    // Update your user in DB to store the new payment method
    // updateUserInDB() is *your* implementation of updating a user in the DB
    updateUserInDB({ paymentMethodID });
  }

  ...
}

We also want to be able to retrieve payment information, so let’s add a method for that as well:

class StripeManager {
  ...

  public static async retreivePaymentInfo(paymentMethodID: string) {
    try {
      const request = await fetch(
        'https://your-endpoint/stripe/retrieve-payment-method',
        {
          method: 'POST',
          body: JSON.stringify({
            paymentMethodID,
          }),
        }
      );

      const result = await request.json();

      return {
        type: result.card.brand,
        digits: result.card.last4,
      };
    } catch (error) {
      console.error(error);
      return null;
    }
  }

  ...
}

Retry invoice

Alright! All that is left is the method for retrying the invoice payment.

class StripeManager {
  ...

  public static async retryInvoice(customerID: string, paymentMethodID: string, invoiceID: string) {
    const request = await fetch('https://your-endpoint/stripe/retry-invoice', {
      method: 'POST',
      body: JSON.stringify({
        customerID,
        paymentMethodID,
        invoiceID,
      }),
    });

    await request.json();
  }

  ...
}

That’s it! 💪 The StripeManager is done!

Payment form

Now, let’s move on to the fun part: The payment form. We’re going to cut some corners here, in terms of the React code. But we’ll make sure that the point is clear.

First, you need to go to your Stripe dashboard -> Developers -> API keys and grab the public key. Let’s store that in an .env file:

REACT_APP_STRIPE_KEY=<PUT-YOUR-KEY-HERE>

Next, let’s install a couple of Stripe dependencies:

npm install @stripe/react-stripe-js @stripe/stripe-js

Let’s create a new component for the payment form: src/component/upgrade.tsx

Now, we start by importing the needed dependencies:

import { loadStripe } from '@stripe/stripe-js';
import {
  CardNumberElement,
  CardExpiryElement,
  CardCvcElement,
  Elements,
  useStripe,
  useElements,
} from '@stripe/react-stripe-js';

ℹ️ You can also import a single element for the whole credit card: `import { CardElement } from '@stripe/react-stripe-js'` In this example we will use individual elements for card number, expiry date and cvc.

Let’s start creating the React component:

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_KEY || '');

const Upgrade = () => {
  return (
    <div>
      <h1>Upgrade now</h1>
      <Elements stripe={stripePromise}>
        <CheckoutForm />
      </Elements>
    </div>
  );
};

const CheckoutForm = () => {
  // Not implemented yet
};

export default Upgrade;

The Upgrade component is going the be the container of the payment form. The form is wrapped in the Elements component that is provided by react-stripe-react. We need to pass a ‘promisified’ instance on Stripe to this component.

Now, let’s create the CheckoutForm component:

const CheckoutForm = () => {
  // Include these hooks:
  const stripe = useStripe();
  const elements = useElements();
  const [nameInput, setNameInput] = useState('');
  const [retry, setRetry] = useState(!!localStorage.getItem('invoice_retry'));

  const handleSubmitPayment = async () => {
    // Not implemented yet
  };

  const handleRetryPayment = async () => {
    // Not implemented yet
  };

  const buttonAction = retry ? handleRetryPayment : handleSubmitPayment;

  return (
    <div>
      <input
        placeholder='Name on card'
        value={nameInput}
        onChange={(e) => setNameInput(e.currentTarget.value)}
      />
      <CardNumberElement />
      <CardExpiryElement />
      <CardCvcElement />
      <button onClick={buttonAction}></button>
    </div>
  );
};

You should be able to see your payment form render 😎

Now, let’s implement that handleSubmitPayment function:

const handleSubmitPayment = async () => {
  if (!stripe || !elements) {
    return;
  }

  try {
    const { error, paymentMethod } = await stripe.createPaymentMethod({
      type: 'card',
      card: elements.getElement(CardNumberElement) as any,
      billing_details: {
        name: nameInput,
      },
    });

    if (error || !paymentMethod) {
      throw Error(error?.message || 'Something is not right...');
    }

    const customerID = await StripeManager.getStripeCustomerID();

    if (!customerID) {
      throw Error('Could not identify customer');
    }
    const paymentID = paymentMethod.id;
    const subscription = await StripeManager.createSubscription(customerID, paymentID);

    if (subscription.latest_invoice.payment_intent.status === 'requires_payment_method') {
      setRetry(true);
      localStorage.setItem('latest_invoice_id', subscription.latest_invoice.id);
      throw Error('Your card was declined. Please try again or with another card');
    }

    if (subscription.status !== 'active') {
      throw Error('Could not process payment.');
    }
  } catch (error) {
    console.error(error);
    // Let the user know that something went wrong here...
  }
};

Finally, let’s implement the handleRetryPayment function:

const handleRetryPayment = async () => {
  if (!stripe || !elements) {
    return;
  }

  const invoiceID = localStorage.getItem('latest_invoice_id');

  try {
    if (!invoiceID) {
      throw Error('Could not process payment. Please refresh and try again.');
    }

    const { error, paymentMethod } = await stripe.createPaymentMethod({
      type: 'card',
      card: elements.getElement(CardNumberElement) as any,
      billing_details: {
        name: nameOfCard,
      },
    });

    if (error || !paymentMethod) {
      throw Error(error?.message || 'Something is not right...');
    }

    const customerID = await StripeManager.getStripeCustomerID();

    if (!customerID) {
      throw Error('Could not identify customer');
    }

    const paymentID = paymentMethod.id;
    await StripeManager.retryInvoice(customerID, paymentID, invoiceID);

    localStorage.removeItem('latest_invoice_id');

  } catch (error) {
    console.error(error);
    // Let the user know that something went wrong here...
  }
};

That’s it! 😎 The payment form should now be working! Now you just need to add some styling, maybe some extra error handling for the name-of-card input, etc.

Manage current subscription

The last thing we need to create is a way of letting the user manage their subscription. As a minimum, they should be able to cancel their subscription, see and change their payment method.

Let’s implement this in a new component. For Sigmetic, we have something that looks like this:

Let’s create a new component: src/components/Plan.tsx

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_KEY || '');

const Plan = () => {
  const [cardInformation, setCardInformation] = useState<{
    type: string;
    digits: string;
  }>();
  const [subscription, setSubscription] = useState<IStripeSubscription>();

  const fetchCardInformation = async () => {
    try {
      const info = await StripeManager.retreivePaymentInfo(user.paymentMethodID);
      if (info) {
        setCardInformation(info);
      }
    } catch (error) {
      // Let the user know that something went wrong here...
    }
  };

  const fetchSubscription = async () => {
    try {
      const sub = await StripeManager.retrieveSubscription(user.subscriptionID);
      if (sub) {
        setSubscription(sub);
      }

    } catch (error) {
      // Let the user know that something went wrong here...
    }
  };

  useEffect(() => {
    fetchSubscription();
    fetchCardInformation();
  }, []);

  const handleCancelSubscription = (end: boolean) => async () => {
    // Not implemented yet
  };

  const expirationDate = new Date(subscription.current_period_end * 1000).toDateString();
  const subscriptionWillEnd = subscription.cancel_at_period_end;

  return (
    <div>
      <div>The plan will expire on: {expirationDate}</div>
      <div>Card: {cardInformation.type}</div>
      <div>**** **** **** {cardInformation.digits}</div>
      <Elements stripe={stripePromise}>
        <UpdateForm />
      </Elements>
      <button onClick={handleCancelSubscription(!subscriptionWillEnd)}>
        {subscriptionWillEnd ? 'Continue' : 'Cancel'}
      </button>
    </div>
  );
};

const UpdateForm = () => {
  // Not implemented yet
};

export default Plan;

We skipped a lot of markup and styling here — that one will be all on you 😉

Let’s implement the handleCancelSubscription method:

const handleCancelSubscription = (end: boolean) => async () => {
  try {
    const subscription = await StripeManager.handleSubscription(user.subscriptionID, end);
    setSubscription(subscription);
  } catch (error) {
    // Let the user know that something went wrong here...
  }
};

Pretty straight forward, right! Finally, let’s implement the UpdateForm component.

const UpdateForm = () => {
  // Include these hooks:
  const stripe = useStripe();
  const elements = useElements();
  const [nameInput, setNameInput] = useState('');

  const handleUpdatePaymentMethod = async () => {
    // Not implemented yet
  };

  return (
    <div>
      <input
        placeholder='Name on card'
        value={nameInput}
        onChange={(e) => setNameInput(e.currentTarget.value)}
      />
      <CardNumberElement />
      <CardExpiryElement />
      <CardCvcElement />
      <button onClick={handleUpdatePaymentMethod}></button>
    </div>
  );
};

And finally, the handleUpdatePaymentMethod function:

const handleUpdatePaymentMethod = async () => {
    if (!stripe || !elements) {
      return;
    }

    try {
      const { error, paymentMethod } = await stripe.createPaymentMethod({
        type: 'card',
        card: elements.getElement(CardNumberElement) as any,
        billing_details: {
          name: nameOfCard,
        },
      });

      if (error || !paymentMethod) {
        throw Error(error?.message || 'Something is not right...');
      }

      const customerID = await StripeManager.getStripeCustomerID();

      if (!customerID) {
        throw Error('Could not identify customer');
      }

      const paymentID = paymentMethod.id;
      await StripeManager.updatePaymentMethod(customerID, paymentID);
      // You may want to refetch the payment method after this...

    } catch (error) {
      Toast.error(error.message);
    }
  };

Rounding up

That’s it 🎉🎉 We’re done with the integration!

Of course, we left out quite a bit of styling, database connections, etc. These are specific to your application and not overly relevant for this article.

As mentioned in the beginning — there are a lot of different ways to handle this, and this article simply explains how we handle it at Sigmetic!

Did you see something unusual, odd, or insecure? Please don’t hesitate to reach out!! We’d like to know.

Any questions or confusion? Please reach out as well — we’ll be happy to answer any questions that may have arisen 🔥