Skip to content

Commit

Permalink
feat: new StripePaymentInput component
Browse files Browse the repository at this point in the history
  • Loading branch information
aldeed committed Jan 9, 2019
1 parent 0473b48 commit a0105f5
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 3 deletions.
113 changes: 113 additions & 0 deletions package/src/components/StripePaymentInput/v1/StripePaymentInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withComponents } from "@reactioncommerce/components-context";
import styled from "styled-components";
import { addTypographyStyles, CustomPropTypes } from "../../../utils";

const SecureCaption = styled.div`
${addTypographyStyles("StripePaymentInputCaption", "captionText")}
`;

const IconLockSpan = styled.span`
display: inline-block;
height: 20px;
width: 20px;
`;

const Span = styled.span`
vertical-align: super;
`;

class StripePaymentInput extends Component {
static propTypes = {
/**
* You can provide a `className` prop that will be applied to the outermost DOM element
* rendered by this component. We do not recommend using this for styling purposes, but
* it can be useful as a selector in some situations.
*/
className: PropTypes.string,
/**
* If you've set up a components context using
* [@reactioncommerce/components-context](https://github.com/reactioncommerce/components-context)
* (recommended), then this prop will come from there automatically. If you have not
* set up a components context or you want to override one of the components in a
* single spot, you can pass in the components prop directly.
*/
components: PropTypes.shape({
/**
* Secured lock icon
*/
iconLock: PropTypes.node,
/**
* Pass either the Reaction StripeForm component or your own component that
* accepts compatible props.
*/
StripeForm: CustomPropTypes.component.isRequired
}),
/**
* Is the payment input being saved?
*/
isSaving: PropTypes.bool,
/**
* When this action's input data switches between being
* ready for saving and not ready for saving, this will
* be called with `true` (ready) or `false`
*/
onReadyForSaveChange: PropTypes.func,
/**
* Called with an object value when this component's `submit`
* method is called. The object may have `data`, `displayName`,
* and `amount` properties.
*/
onSubmit: PropTypes.func
};

static defaultProps = {
onReadyForSaveChange() {},
onSubmit() {}
};

componentDidMount() {
const { onReadyForSaveChange } = this.props;
onReadyForSaveChange(false);
}

async submit() {
const { onSubmit } = this.props;
const { token } = await this._stripe.createToken();

await onSubmit({
displayName: `${token.card.brand} ending in ${token.card.last4}`,
data: {
stripeTokenId: token.id
}
});
}

handleChangeReadyState = (isReady) => {
const { onReadyForSaveChange } = this.props;

if (isReady !== this.lastIsReady) {
onReadyForSaveChange(isReady);
}
this.lastIsReady = isReady;
}

render() {
const { className, components: { iconLock, StripeForm } } = this.props;

return (
<div className={className}>
<StripeForm
isComplete={this.handleChangeReadyState}
stripeRef={(stripe) => { this._stripe = stripe; }}
/>
<SecureCaption>
<IconLockSpan>{iconLock}</IconLockSpan> <Span>Your Information is private and secure.</Span>
</SecureCaption>
</div>
);
}
}

export default withComponents(StripePaymentInput);
42 changes: 42 additions & 0 deletions package/src/components/StripePaymentInput/v1/StripePaymentInput.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
### Overview

The `StripePaymentInput` component is intended to be used as the `InputComponent` for the `stripe_card` payment method in a Reaction client UI. Provide it in the `paymentMethods` array passed to the [PaymentsCheckoutAction](./#!/PaymentsCheckoutAction) component.

### Usage

```jsx
class Example extends React.Component {
constructor(props) {
super(props);

this.state = { isReady: false };
}

render() {
return (
<div>
<StripePaymentInput
ref={(ref) => { this.form = ref; }}
onChange={(...args) => { console.log("onChange", ...args); }}
onReadyForSaveChange={(isReady) => {
console.log("onReadyForSaveChange", isReady);
this.setState({ isReady });
}}
onSubmit={(doc) => { console.log("onSubmit", doc); }}
/>
<Button isDisabled={!this.state.isReady} onClick={() => { this.form.submit(); }}>Submit</Button>
</div>
);
}
}

<Example />
```

### Theme

Assume that any theme prop that does not begin with "rui" is within `rui_components`. See [Theming Components](./#!/Theming%20Components).

#### Typography

- The "Your Information is private and secure" text uses `captionText` style with `rui_components.StripePaymentInputCaption` override
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from "react";
import renderer from "react-test-renderer";
import { mount } from "enzyme";
import { StripeProvider } from "react-stripe-elements";
import { ComponentsProvider } from "@reactioncommerce/components-context";
import mockComponents from "../../../tests/mockComponents";
import realComponents from "../../../tests/realComponents";
import StripePaymentInput from "./StripePaymentInput";

// Mock the Stripe instance
const elementMock = {
mount: jest.fn(),
destroy: jest.fn(),
on: jest.fn(),
update: jest.fn()
};
const elementsMock = {
create: jest.fn().mockReturnValue(elementMock)
};
const stripeMock = {
elements: jest.fn().mockReturnValue(elementsMock),
createToken: jest.fn(),
createSource: jest.fn()
};

test("basic snapshot", () => {
const component = renderer.create(<StripePaymentInput components={mockComponents} />);

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test("calls onReadyForSaveChange on mount and change", () => {
const onReadyForSaveChange = jest.fn();

let formEl;
mount((
<StripeProvider stripe={stripeMock}>
<ComponentsProvider value={realComponents}>
<StripePaymentInput
ref={(ref) => { formEl = ref; }}
onReadyForSaveChange={onReadyForSaveChange}
/>
</ComponentsProvider>
</StripeProvider>
));

expect(onReadyForSaveChange).toHaveBeenCalledTimes(1);
expect(onReadyForSaveChange).toHaveBeenLastCalledWith(false);

onReadyForSaveChange.mockClear();

formEl.handleChangeReadyState(true);

expect(onReadyForSaveChange).toHaveBeenCalledTimes(1);
expect(onReadyForSaveChange).toHaveBeenLastCalledWith(true);
});

test("calls onSubmit when submit method is called", (done) => {
const onSubmit = jest.fn();

stripeMock.createToken.mockReturnValueOnce(Promise.resolve({
token: {
card: {
brand: "Visa",
last4: "1234"
},
id: "abc123"
}
}));

let formEl;
mount((
<StripeProvider stripe={stripeMock}>
<ComponentsProvider value={realComponents}>
<StripePaymentInput ref={(ref) => { formEl = ref; }} onSubmit={onSubmit} />
</ComponentsProvider>
</StripeProvider>
));

expect(onSubmit).not.toHaveBeenCalled();

formEl.submit();

setTimeout(() => {
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenLastCalledWith({
data: {
stripeTokenId: "abc123"
},
displayName: "Visa ending in 1234"
});
done();
}, 0);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`basic snapshot 1`] = `
.c0 {
-webkit-font-smoothing: antialiased;
color: #b3b3b3;
font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,sans-serif;
font-size: 14px;
font-style: normal;
font-stretch: normal;
font-weight: 400;
-webkit-letter-spacing: .02em;
-moz-letter-spacing: .02em;
-ms-letter-spacing: .02em;
letter-spacing: .02em;
line-height: 1.25;
}
.c1 {
display: inline-block;
height: 20px;
width: 20px;
}
.c2 {
vertical-align: super;
}
<div
className={undefined}
>
StripeForm({})
<div
className="c0"
>
<span
className="c1"
/>
<span
className="c2"
>
Your Information is private and secure.
</span>
</div>
</div>
`;
1 change: 1 addition & 0 deletions package/src/components/StripePaymentInput/v1/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./StripePaymentInput";
7 changes: 4 additions & 3 deletions styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,15 +481,17 @@ module.exports = {
generateSection({
componentNames: [
"CheckoutAction",
"CheckoutActions",
"CheckoutActionComplete",
"CheckoutActionIncomplete",
"CheckoutActions",
"CheckoutEmailAddress",
"CheckoutTopHat",
"ExampleIOUPaymentForm",
"FinalReviewCheckoutAction",
"FulfillmentOptionsCheckoutAction",
"ShippingAddressCheckoutAction",
"StripePaymentCheckoutAction",
"FulfillmentOptionsCheckoutAction"
"StripePaymentInput"
],
content: "styleguide/src/sections/Checkout.md",
name: "Checkout"
Expand All @@ -500,7 +502,6 @@ module.exports = {
"AddressChoice",
"AddressForm",
"AddressReview",
"ExampleIOUPaymentForm",
"GuestForm",
"StripeForm"
],
Expand Down

0 comments on commit a0105f5

Please sign in to comment.