Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use react-stripe-js? #16037

Closed
2 tasks done
gvetsa opened this issue Jun 3, 2019 · 42 comments · Fixed by #16399
Closed
2 tasks done

How to use react-stripe-js? #16037

gvetsa opened this issue Jun 3, 2019 · 42 comments · Fixed by #16399
Assignees
Labels
component: text field This is the name of the generic UI component, not the React module! docs Improvements or additions to the documentation

Comments

@gvetsa
Copy link

gvetsa commented Jun 3, 2019

When changing any Stripe input the InputBase.js produces an error in the dirtyCheck when trying to access the value of either the inputRef.current or target.value: Cannot read property 'value' of undefined

  • This is not a v0.x issue.
  • I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior 🤔

Input should update normally

Current Behavior 😯

Produces error and prevents using input

Steps to Reproduce 🕹

Link: https://codesandbox.io/s/createreactapp-1e4fe

  1. Enter a number
  2. Check the console for error, it should say Cannot read property 'value' of undefined

Context 🔦

Error causes inability to proceed in payment screen

Your Environment 🌎

Tech Version
Material-UI v4.0.2
React v16.8.6
Browser Chrome Version 74.0.3729.169
TypeScript N/A
etc.

Screen Shot 2019-06-03 at 1 48 28 PM
Screen Shot 2019-06-03 at 1 48 41 PM

@oliviertassinari oliviertassinari added external dependency Blocked by external dependency, we can’t do anything about it component: text field This is the name of the generic UI component, not the React module! labels Jun 3, 2019
@oliviertassinari

This comment has been minimized.

@oliviertassinari oliviertassinari added the good first issue Great for first contributions. Enable to learn the contribution process. label Jun 3, 2019
@eps1lon

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@eps1lon

This comment has been minimized.

@oliviertassinari oliviertassinari added docs Improvements or additions to the documentation and removed good first issue Great for first contributions. Enable to learn the contribution process. labels Jun 4, 2019
@oliviertassinari oliviertassinari changed the title Changing input value for uncontrolled Stripe input causes error for dirtyCheck function in InputBase.js How to use react-stripe-elements? Jun 4, 2019
@oliviertassinari
Copy link
Member

oliviertassinari commented Jun 4, 2019

A codesandbox that demonstrates how to do the integration: https://codesandbox.io/s/stripe-0xez4 with

Dependency Version
@mui/material ^5.0.0
@stripe/react-stripe-js ^1.4.1

Capture d’écran 2021-06-20 à 18 55 11

App.tsx

import * as React from "react";
import Container from "@mui/material/Container";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import CreditCardDetail from "./CreditCardDetail";

const stripePromise = loadStripe("pk_test_ju6veMmqd5eDMe1XhQVPyze2");

export default function App() {
  return (
    <Container maxWidth="sm" sx={{ my: 4 }}>
      <Elements stripe={stripePromise}>
        <CreditCardDetail />
      </Elements>
    </Container>
  );
}

CreditCardDetail.tsx

import * as React from "react";
import Grid from "@mui/material/Grid";
import {
  StripeTextFieldNumber,
  StripeTextFieldExpiry,
  StripeTextFieldCVC
} from "./commonTextFields";

export default function CreditCardDetail() {
  const [state, setState] = React.useState({
    cardNumberComplete: false,
    expiredComplete: false,
    cvcComplete: false,
    cardNumberError: null,
    expiredError: null,
    cvcError: null
  });

  const onElementChange = (field, errorField) => ({
    complete,
    error = { message: null }
  }) => {
    setState({ ...state, [field]: complete, [errorField]: error.message });
  };

  const { cardNumberError, expiredError, cvcError } = state;

  return (
    <Grid container spacing={2}>
      <Grid item xs={12} md={6}>
        <StripeTextFieldNumber
          error={Boolean(cardNumberError)}
          labelErrorMessage={cardNumberError}
          onChange={onElementChange("cardNumberComplete", "cardNumberError")}
        />
      </Grid>
      <Grid item xs={6} sm={3}>
        <StripeTextFieldExpiry
          error={Boolean(expiredError)}
          labelErrorMessage={expiredError}
          onChange={onElementChange("expiredComplete", "expiredError")}
        />
      </Grid>
      <Grid item xs={6} sm={3}>
        <StripeTextFieldCVC
          error={Boolean(cvcError)}
          labelErrorMessage={cvcError}
          onChange={onElementChange("cvcComplete", "cvcError")}
        />
      </Grid>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </Grid>
  );
}

commonTextFields.tsx

import * as React from "react";
import {
  AuBankAccountElement,
  CardCvcElement,
  CardExpiryElement,
  CardNumberElement,
  FpxBankElement,
  IbanElement,
  IdealBankElement
} from "@stripe/react-stripe-js";
import TextField, { TextFieldProps } from "@mui/material/TextField";
import StripeInput from "./StripeInput";

type StripeElement =
  | typeof AuBankAccountElement
  | typeof CardCvcElement
  | typeof CardExpiryElement
  | typeof CardNumberElement
  | typeof FpxBankElement
  | typeof IbanElement
  | typeof IdealBankElement;

interface StripeTextFieldProps<T extends StripeElement>
  extends Omit<TextFieldProps, "onChange" | "inputComponent" | "inputProps"> {
  inputProps?: React.ComponentProps<T>;
  labelErrorMessage?: string;
  onChange?: React.ComponentProps<T>["onChange"];
  stripeElement?: T;
}

export const StripeTextField = <T extends StripeElement>(
  props: StripeTextFieldProps<T>
) => {
  const {
    helperText,
    InputLabelProps,
    InputProps = {},
    inputProps,
    error,
    labelErrorMessage,
    stripeElement,
    ...other
  } = props;

  return (
    <TextField
      fullWidth
      InputLabelProps={{
        ...InputLabelProps,
        shrink: true
      }}
      error={error}
      InputProps={{
        ...InputProps,
        inputProps: {
          ...inputProps,
          ...InputProps.inputProps,
          component: stripeElement
        },
        inputComponent: StripeInput
      }}
      helperText={error ? labelErrorMessage : helperText}
      {...(other as any)}
    />
  );
};

export function StripeTextFieldNumber(
  props: StripeTextFieldProps<typeof CardNumberElement>
) {
  return (
    <StripeTextField
      label="Credit Card Number"
      stripeElement={CardNumberElement}
      {...props}
    />
  );
}

export function StripeTextFieldExpiry(
  props: StripeTextFieldProps<typeof CardExpiryElement>
) {
  return (
    <StripeTextField
      label="Expires"
      stripeElement={CardExpiryElement}
      {...props}
    />
  );
}

export function StripeTextFieldCVC(
  props: StripeTextFieldProps<typeof CardCvcElement>
) {
  return (
    <StripeTextField
      label="CVC Code"
      stripeElement={CardCvcElement}
      {...props}
    />
  );
}
import * as React from "react";
import { alpha, useTheme } from "@mui/material/styles";
import { InputBaseComponentProps } from "@mui/material/InputBase";

const StripeInput = React.forwardRef<any, InputBaseComponentProps>(
  function StripeInput(props, ref) {
    const { component: Component, options, ...other } = props;
    const theme = useTheme();
    const [mountNode, setMountNode] = React.useState<any | null>(null);

    React.useImperativeHandle(
      ref,
      () => ({
        focus: () => mountNode.focus()
      }),
      [mountNode]
    );

    return (
      <Component
        onReady={setMountNode}
        options={{
          ...options,
          style: {
            base: {
              color: theme.palette.text.primary,
              fontSize: "16px",
              lineHeight: "1.4375em", // 23px
              fontFamily: theme.typography.fontFamily,
              "::placeholder": {
                color: alpha(theme.palette.text.primary, 0.42)
              }
            },
            invalid: {
              color: theme.palette.text.primary
            }
          }
        }}
        {...other}
      />
    );
  }
);

export default StripeInput;

A related issue: stripe-archive/react-stripe-elements#164.

@eps1lon

This comment has been minimized.

@gvetsa

This comment has been minimized.

@eps1lon

This comment has been minimized.

@gvetsa

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@joshwooding

This comment has been minimized.

@eps1lon

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@ilovett

This comment has been minimized.

@TrejGun

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@TrejGun

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@TrejGun

This comment has been minimized.

@kunal-mandalia

This comment has been minimized.

@TrejGun

This comment has been minimized.

@WillSquire

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@mweibel

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@mweibel

This comment has been minimized.

@HaNdTriX

This comment has been minimized.

@WillSquire

This comment has been minimized.

@picosam

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@picosam

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@picosam

This comment has been minimized.

@oseisaac

This comment has been minimized.

@dorgnarg

This comment has been minimized.

@ffMathy

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@victor-yunenko

This comment has been minimized.

@oliviertassinari oliviertassinari changed the title How to use react-stripe-elements? How to use react-stripe-js? Jun 23, 2021
@dmitrif
Copy link

dmitrif commented Aug 7, 2021

#16037 (comment) wasn't working for me with the following:

Dependency Version
@material-ui/core 4.11.0
@stripe/react-stripe-js 1.4.1

The issue is that MUI is attempting to pass in a prop named inputRef instead of ref which would be forwarded. Beyond that I cleaned up the typing, but would appreciate help getting rid of the 2 @ts-ignore in commonTextFields (for now I have them ignored, and the code obviously works):

  • Type 'FormEvent<HTMLInputElement | HTMLTextAreaElement>' is missing the following properties from type 'StripeCardNumberElementChangeEvent': elementType, brand, empty, complete, errors
  • Type '(props: Props) => JSX.Element' is not assignable to type 'ElementType<InputBaseComponentProps> | undefined'.

Anyways, here is the code:

commonTextFields.tsx

import React from 'react';
import {
  AuBankAccountElement,
  CardCvcElement,
  CardExpiryElement,
  CardNumberElement,
  FpxBankElement,
  IbanElement,
  IdealBankElement,
} from '@stripe/react-stripe-js';
import TextField, { TextFieldProps } from '@material-ui/core/TextField';
import StripeInput from './StripeInput';

export type StripeElement =
  | typeof AuBankAccountElement
  | typeof CardCvcElement
  | typeof CardExpiryElement
  | typeof CardNumberElement
  | typeof FpxBankElement
  | typeof IbanElement
  | typeof IdealBankElement;

type StripeTextFieldProps<T extends StripeElement> = Omit<
  TextFieldProps,
  'onChange' | 'inputComponent' | 'inputProps'
> & {
  inputProps?: React.ComponentProps<T>;
  labelErrorMessage?: string;
  onChange?: React.ComponentProps<T>['onChange'];
  stripeElement?: T;
};

const StripeTextDefaultProps = {
  inputProps: {},
  labelErrorMessage: '',
  onChange: () => {},
  stripeElement: null,
};

export const StripeTextField = <T extends StripeElement>(
  props: StripeTextFieldProps<T>
): JSX.Element => {
  const {
    helperText,
    InputLabelProps,
    InputProps = {},
    inputProps,
    error,
    labelErrorMessage,
    stripeElement,
    ...other
  } = props;

  return (
    <TextField
      fullWidth
      variant="outlined"
      InputLabelProps={{
        ...InputLabelProps,
        shrink: true,
      }}
      error={error}
      InputProps={{
        ...InputProps,
        // @ts-ignore
        inputProps: {
          ...inputProps,
          ...InputProps.inputProps,
          component: stripeElement,
        },
        // @ts-ignore
        inputComponent: StripeInput,
      }}
      helperText={error ? labelErrorMessage : helperText}
      {...other}
    />
  );
};

StripeTextField.defaultProps = StripeTextDefaultProps;

export function StripeTextFieldNumber(
  props: StripeTextFieldProps<typeof CardNumberElement>
): JSX.Element {
  return (
    <StripeTextField
      {...props}
      label="Credit Card Number"
      stripeElement={CardNumberElement}
    />
  );
}

StripeTextFieldNumber.defaultProps = StripeTextDefaultProps;

export function StripeTextFieldExpiry(
  props: StripeTextFieldProps<typeof CardExpiryElement>
): JSX.Element {
  return (
    <StripeTextField
      {...props}
      label="Expires"
      stripeElement={CardExpiryElement}
    />
  );
}

StripeTextFieldExpiry.defaultProps = StripeTextDefaultProps;

export function StripeTextFieldCVC(
  props: StripeTextFieldProps<typeof CardCvcElement>
): JSX.Element {
  return (
    <StripeTextField
      {...props}
      label="CVC Code"
      stripeElement={CardCvcElement}
    />
  );
}

StripeTextFieldCVC.defaultProps = StripeTextDefaultProps;

CreditCardDetail.tsx

import * as React from 'react';
import Grid from '@material-ui/core/Grid';
import {
  StripeTextFieldNumber,
  StripeTextFieldExpiry,
  StripeTextFieldCVC,
} from './commonTextFields';

export default function CreditCardDetail(): JSX.Element {
  const [state, setState] = React.useState({
    cardNumberComplete: false,
    expiredComplete: false,
    cvcComplete: false,
    cardNumberError: undefined,
    expiredError: undefined,
    cvcError: undefined,
  });

  const onElementChange = (field: string, errorField: string) => ({
    complete,
    error = { message: null },
  }: {
    complete: boolean;
    error?: { message: null | string };
  }) => {
    setState({ ...state, [field]: complete, [errorField]: error.message });
  };

  const { cardNumberError, expiredError, cvcError } = state;

  return (
    <Grid container spacing={2}>
      <Grid item xs={12} md={6}>
        <StripeTextFieldNumber
          error={Boolean(cardNumberError)}
          labelErrorMessage={cardNumberError}
          onChange={onElementChange('cardNumberComplete', 'cardNumberError')}
        />
      </Grid>
      <Grid item xs={6} sm={3}>
        <StripeTextFieldExpiry
          error={Boolean(expiredError)}
          labelErrorMessage={expiredError}
          onChange={onElementChange('expiredComplete', 'expiredError')}
        />
      </Grid>
      <Grid item xs={6} sm={3}>
        <StripeTextFieldCVC
          error={Boolean(cvcError)}
          labelErrorMessage={cvcError}
          onChange={onElementChange('cvcComplete', 'cvcError')}
        />
      </Grid>
    </Grid>
  );
}

StripeInput.tsx

import React from 'react';
import { useTheme } from '@material-ui/core';
import { fade } from '@material-ui/core/styles';
import { InputBaseComponentProps } from '@material-ui/core/InputBase';

type Props = InputBaseComponentProps & {
  component: React.FunctionComponent<InputBaseComponentProps>;
  options: Record<string, unknown>;
};

const StripeInputComponent = (props: Props): JSX.Element => {
  const { component: Component, options, inputRef, ...other } = props;
  const theme = useTheme();
  const [mountNode, setMountNode] = React.useState<HTMLInputElement | null>(
    null
  );

  React.useImperativeHandle(
    inputRef,
    () => ({
      focus: () => {
        if (mountNode === null) {
          return;
        }

        mountNode.focus();
      },
    }),
    [mountNode]
  );

  return (
    <Component
      onReady={setMountNode}
      options={{
        ...options,
        style: {
          base: {
            color: theme.palette.text.primary,
            fontSize: '16px',
            lineHeight: '1.4375em', // 23px
            fontFamily: theme.typography.fontFamily,
            '::placeholder': {
              color: fade(theme.palette.text.primary, 0.42),
            },
          },
          invalid: {
            color: theme.palette.text.primary,
          },
        },
      }}
      {...other}
    />
  );
};

export default StripeInputComponent;

@IsaacTrevino
Copy link

I believe this issue has occurred for v5. TextField onChange is not getting triggered.
#26856

@dan-turner

This comment was marked as resolved.

@jlev
Copy link

jlev commented Jan 30, 2023

For MUI v5 I had to wrap the StripeInputComponent in React.forwardRef, per https://mui.com/material-ui/react-text-field/#integration-with-3rd-party-input-libraries

Like this

// modified from https://github.com/mui/material-ui/issues/16037

import React from 'react';
import { InputBaseComponentProps } from '@mui/material/InputBase';

type Props = InputBaseComponentProps & {
  component: React.FunctionComponent<InputBaseComponentProps>;
  options: Record<string, unknown>;
};

const StripeInputComponent = React.forwardRef((props: Props, ref) : JSX.Element => {
  const { component: Component, options, ...other } = props;
  const [mountNode, setMountNode] = React.useState<HTMLInputElement | null>(
    null
  );

  React.useImperativeHandle(
    ref,
    () => ({
      focus: () => {
        if (mountNode === null) {
          return;
        }

        mountNode.focus();
      },
    }),
    [mountNode]
  );

  return (
    <Component
      onReady={setMountNode}
      options={{
        ...options,
      }}
      {...other}
    />
  );
});

export default StripeInputComponent;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: text field This is the name of the generic UI component, not the React module! docs Improvements or additions to the documentation
Projects
None yet
Development

Successfully merging a pull request may close this issue.