#KoalaKey - useEffect

1 messages ยท Page 1 of 1 (latest)

ionic solstice
#

Hello! Unfortunately I'm not very familiar with React, but I'll do what I can to help! Is this code from a guide or tutorial?

graceful lagoon
#

It's just a practice file I came up with. Essentially I am just playing around with this because I would eventually like to render some content on a page based on the results from a larger query

ionic solstice
#

Oh, I think I see the issue.

#

Try putting async before the function and then add await before the stripe.customers.search call.

#

Oh, no... I guess that's not really it... you're using .then on customer...

#

That's an unusual way to go about things. The customer isn't really the Customer, it's a Promise.

graceful lagoon
#

Yeah i had tried that haha. Yeah, i could've named it better but like I said it's just some practice I'm currently doing

ionic solstice
#

Usually Stripe.js would not be initialized inside useEffect. Typically you would do something like this instead:

import React, {useState, useEffect} from 'react';
import {useStripe} from '@stripe/react-stripe-js';

const PaymentStatus = () => {
  const stripe = useStripe();
  const [message, setMessage] = useState(null);

  useEffect(() => {
    if (!stripe) {
      return;
    }
    // Other code...
graceful lagoon
#

Oh wow, okay I'll check that out! thanks for this!

ionic solstice
#

And then earlier you would have done const stripePromise = loadStripe('pk_test_...'); somewhere.

dense nymph
#

(NOT with Stripe) I am pretty familiar with React, if you want a 2nd set of eyes

graceful lagoon
#

Yeah the more the merrier! Been toying with this for a couple days haha

dense nymph
#

Very Basic Point First: useEffect MUST BE SYNCHRONOUS

ionic solstice
#

@dense nymph Thanks for jumping in to help! Also, as an aside, it's okay to not prefix your messages with "(NOT with Stripe)"; we have the Stripe Staff role and badges to indicate who on the server is from Stripe. ๐Ÿ™‚

dense nymph
#

That's not for your benefit; it's for everyone else's so they don't put undue faith in me

graceful lagoon
#

So, calling async functions inside is basically a no go?

#

inside of useEffect **

dense nymph
#

So: you can fire off a closure to run an asynchronous function, but you really need to be careful. But by definition and design, useEffect cannot be asynchronous

ionic solstice
#

That's fair, and if you'd like to keep including a disclaimer that's fine, but might I suggest moving it to the end of your messages? When I first saw you use it I thought you were indicating you couldn't do something on Stripe. ๐Ÿ˜„

ionic solstice
#

I will now be quiet so you two can sort out the React issue. ๐Ÿ™‚

dense nymph
#

so: stripe needs to be initiated, but not with useEffect. I can show you my paymentEmbed code, if you like - I expect I'll like publish some of this stuff, maybe as libraries, at some point soon

#

Step one: it is recommended to initialize Stripe at a fairly HIGH level in your code - it gives Radar and other Fraud systems far more signals to work with

graceful lagoon
#

yeah that would be great if you could. I tried initializing stripe outside of the useEffect block initially but I was still running into the same problem of nothing being logged to the console. the initialization within the useEffect was just my latest code

dense nymph
#

so I have this loaded as part of quite a library of code:

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

export let stripe = null;
export let stripe_started = false;
export let stripe_running = false;

(async () => {
  try {
    stripe = await loadStripe("pk_test_xxxx");
    stripe_running = true;
  } catch (err) {
    stripe_running = false;
  } finally {
    stripe_started = true;
  }
})();
#

Note the closure, as there is no top-level await in a browser

graceful lagoon
#

oh I see what you are doing now

dense nymph
#

the impoprt of this starts in top-level app.js

#

THEN

#

(couple mo's for formats, etc)

#

(too many tabs open)

graceful lagoon
#

haha I feel that. I always have a ton open

dense nymph
#

Raect Component Outer:

import React, { useEffect, useState } from "react";
import { SpinnerDiamond } from "spinners-react";
import { Elements } from "@stripe/react-stripe-js";
import PaymentInput from "../PaymentInput";
import Popup from "reactjs-popup";
import {
  capturePaymentMethod,
  stripe,
  PAYMENT_THEME,
  fixPledgeTicket,
  logger
} from "../../services";

export default function PaymentPopupButton(props) {
  const { fan, status } = props;

  const [open, setOpen] = useState(false);
  const closeModal = () => setOpen(false);

  useEffect(() => {
    closeModal();
  }, [fan]);

  return (
    <>
      <div
        type="cell button expanded rounded"
        className={`cell button expanded rounded ${status}`}
        onClick={() => setOpen((o) => !o)}
      >
        {`Payment Method: ${
          fan?.payment_method ? fan.payment_method : "Create One Now"
        }`}
      </div>
      <Popup
        open={open}
        onClose={closeModal}
        modal
        nested={true}
        closeOnDocumentClick
      >
        <PaymentEmbed fan={fan} />
      </Popup>
    </>
  );
}
#

export const PaymentEmbed = (props) => {
  const { fan } = props;

  const [options, setOptions] = useState(null);

  const [secret, setSecret] = useState(null);

  useEffect(() => {
    if (!secret) {
      (async () => {
        //this process INITIATES generation of a payment_secret
        const result = await capturePaymentMethod({ personId: fan.Id });
        result?.data && setSecret(result.data.payment_secret);
      })();
    } else {
      setOptions({
        // passing the client secret obtained in step 2
        clientSecret: secret,
        // Fully customizable with appearance API.
        appearance: {
          theme: PAYMENT_THEME
          /*...*/
        }
      });
    }
  }, [fan, secret]);

  const doConfirm = async (...args) => {
    return stripe.confirmSetup(...args);
  };

  return options ? (
    <Elements stripe={stripe} options={options}>
      <PaymentInput confirming={doConfirm} />
    </Elements>
  ) : (
    <SpinnerDiamond />
  );
};
#

There's a few application specific bits there, but I don't think they'll distract

graceful lagoon
#

No no this is good! I really appreciate it!

dense nymph
#

The PaymentEmbed is the more relevant part

#

the createPaymentMethod is a remote server call - I use Firebase Cloud Function Triggers

graceful lagoon
#

Wait so am I even loading in stripe the correct way if I want to call the apis?

#

because it looks like you are using the stripe js sdk

#

which I do not think has the ability to call the customer apis?

#

at least I never saw a way in the docs to do so

dense nymph
#

Well, it does, but not using it directly here - you'll see more in the PaymentInput component

#

The docs can be confusing... there are sections for the Server level APIs, and separately for the client APIs

#

...and frustratingly, almost no notations that significant sections are considered deprecated - I've put in a few feature requests to change that

#

so let's share the PaymentInput component, as see if that helps

ionic solstice
#

Typically Stripe.js is used for only a small subset of operations, with most operations requiring your secret key and server-side code.

dense nymph
#

(which is how I'm structured)

ionic solstice
#

Customer operations are typically server-side, for example.

graceful lagoon
#

ahh I see. So essentially what you are saying is that I should not even be querying for the customer metadata where I am?

dense nymph
#

As a rule, if it requires a secret, I only have it on Server side. I've built up a library of server-side functions

graceful lagoon
#

ahhh gotcha, I'll have to look into doing that

dense nymph
#

ask what you need; I'll wait before driving convo offscreen with next code section

graceful lagoon
#

Nahh I'm good you can share it

dense nymph
#

PaymentInput component:

import React, { useState } from "react";
import { SpinnerDiamond } from "spinners-react";
import {
  useStripe,
  useElements,
  PaymentElement
} from "@stripe/react-stripe-js";
import { COMPANY_NAME, ERROR_TYPES, logger } from "../../services";

export default function PaymentInput(props) {
  const { confirming } = props;
  const stripe = useStripe();
  const elements = useElements();

  const [error_message, set_error_message] = useState(null);
  const [showSpinner, setShowSpinner] = useState(true);

  const handleSubmit = async (event) => {
    // We don't want to let default form submission happen here,
    // which would refresh the page.
    event && event.preventDefault();
    if (!stripe || !elements) {
      // Stripe.js has not yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }
    setShowSpinner(true);

    const result = await confirming({
      //`Elements` instance that was used to create the Payment Element
      elements,
      confirmParams: {
        return_url: window.location.href
      },
      redirect: `if_required`
    });

    if (result?.error) {
      switch (result.error.type) {
        case ERROR_TYPES.CARD_ERROR:
        case ERROR_TYPES.VALIDATION_ERROR:
          set_error_message(
            `${
              result.error.message_code
                ? "Code: " + result.error.message_code + " : "
                : ""
            }${result.error.message} Try re-submitting`
          );
          break;
        default:
          break;
      }
      // Show error to your customer (e.g., payment details incomplete)
      logger(result.error.message);
      setShowSpinner(false);
    }
    /**
     * if there is no error, the user record ("People") changes state,
     * and this entire component is removed.
     */
  };
#

  const handleReady = async (event) => {
    // We don't want to let default form submission happen here,
    // which would refresh the page.
    if (!stripe || !elements) {
      // Stripe.js has not yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }

    setShowSpinner(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      {showSpinner && <SpinnerDiamond />}
      <PaymentElement
        onReady={handleReady}
        options={{ business: { name: COMPANY_NAME } }}
      />
      <div>{error_message ? error_message : null}</div>
      <button
        className="button"
        type="submit"
        disabled={!stripe || showSpinner}
      >
        Submit
      </button>
    </form>
  );
}
#

There's a bit of my general philosophy and architecture hidden in here, although noted.

#

My general flow is FrontEnd => stripe => webhook => database => database listener => front end

#

So you may not see a direct "return" for some of my structure

graceful lagoon
#

No this is good! thank you!

dense nymph
#

In this case, if the setupIntent succeeds, the paymentMethod is noted on a "Person" database record - which updates a "listener" which changes an object in Redux

#

If the confirm has errors (or the form has errors), no Redux state change occurs and the errors are displayed

#

OUTSIDE of the code shown, I first check if the Person record has an existing paymentMethod, if so, this component never shows.

#

(to confuse you more - it's tristate - if they're not logged in it pops up a login modal; if the logged in user doesn't have a paymentMethod it pops up the capture modal, else it becomes the "pay" button)

graceful lagoon
#

That's what I would eventually like to do. Check to see if a customer has an active subscription and if they do I want to be able to hide a button on a page. But, I thought the way to do it was to query the API right in the useEffect and set a state.

#

Wow that must be quite the application haha

dense nymph
#

Redux, used for what it's meant for, is useful here. Global State - such as the status of the logged in user. Then it's just a display decision in your components

#

It's a couple hundred thousand lines of application code, a similar number of Cloud Function/Server code, and a similar number of lines of an npm "wrapper" library I built for firebase

#

the last is to share a LOT of common code between frontend and backend

graceful lagoon
#

wow impressive! if you do release the files as a library I'd love to know

dense nymph
#

Does the above help clarify some of the flow involved?

graceful lagoon
#

yeah definitely I appreciate the help! I think I am going to rethink how I go about achieving this

dense nymph
#

I assume you were "just" trying to test a query to stripe?

graceful lagoon
#

yes

dense nymph
#

Gets trickier, because the calls you are using are built for server-side. BUT I believe there are calls to use a "publishable" and/or "ephemeral" key on the front end for limited access. Probably want to look at this documentation: https://stripe.com/docs/js

graceful lagoon
#

Well could I just make my 'query' an api route in my application and then fetch the result and pass it to the front end? that seems like it would make more sense in my case

dense nymph
#

Also, if you haven't done so, really read the developer documentation. I find a LOT of people try to start directly in the API docs, which is gonna lead you down some dark places, especially as the deprecated bits aren't well marked

#

Yes, that is exactly what is intended by stripe

#

Build yourself your own API for your needs to isolate Stripe mostly to the safe server side

#

My own system is a React front end, Google Firebase Cloud Functions as a backend tied together with Firebase Firestore as my DB

graceful lagoon
#

okay that makes much more sense. I haven't really had the need to access the API's as of yet since I am using Auth0 for the user data store and all of my needs up until now I have been able to take care of using Auth0 actions. I've been reading the docs but its definitely a struggle sometimes haha. My application is React front end, Auth0 for security and user store, and Airtable for the datastore

dense nymph
#

Oddly, I started with Airtable, but blew out of it's abilities almost immediately. The biggest reason for my wrapper library was to hide the difference between Firestore and Airtable.

#

My system is a tad more complex than a subscription

graceful lagoon
#

oh wow, yeah I'm not doing anything super crazy like you are haha. Just giving users access to data in Airtable

dense nymph
#

Start with the browser-side API doc, and the developer docs. between them they're a pretty good tutorial. The server-side API docs are a LOT more dry

#

Oh, and probably need to read the Reactjs documentation too, especially the hooks. They have VERY specific requirements.

graceful lagoon
#

Yeah I'll definitely be going back and digging through the docs for sure

#

I appreciate all the help!

dense nymph
#

I'm off; good luck.