Skip to content

Commit

Permalink
Merge pull request #1647 from joincivil/nickreynolds/add-card
Browse files Browse the repository at this point in the history
Add/Remove Card via Account Page Payment Methods Tab
  • Loading branch information
nickreynolds committed Feb 24, 2020
2 parents 8386bf6 + 8c40ed6 commit a865b4d
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 115 deletions.
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"@kirby-web3/parent-core": "^1.3.2",
"@kirby-web3/plugin-ethereum": "^1.11.0",
"@types/styled-components": "^4.1.18",
"apollo-client": "^2.6.4",
"apollo-client": "^2.6.8",
"apollo-storybook-react": "^0.1.8",
"classnames": "^2.2.5",
"graphql-tag": "^2.10.0",
Expand Down
63 changes: 12 additions & 51 deletions packages/components/src/Payments/PaymentIntentsStripeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
CREATE_PAYMENT_METHOD,
CLONE_PAYMENT_METHOD,
} from "./queries";
import { injectStripe, ReactStripeElements, CardElement } from "react-stripe-elements";
import { injectStripe, ReactStripeElements } from "react-stripe-elements";
import styled from "styled-components";
import { PaymentsFormWrapper } from "./PaymentsFormWrapper";
import { CivilContext, ICivilContext } from "../context";
Expand All @@ -24,27 +24,14 @@ import {
import {
PayWithCardText,
PaymentStripeNoticeText,
PaymentEmailConfirmationText,
PaymentTermsText,
PaymentErrorText,
PaymentEmailPrepopulatedText,
} from "./PaymentsTextComponents";
import {
InputValidationUI,
InputStripeValidationUI,
StripeElement,
InputErrorMessage,
} from "./PaymentsInputValidationUI";
import { INPUT_STATE } from "./types";
import { Checkbox, CheckboxSizes } from "../input";
import { PaymentStripeFormSavedCard } from "./PaymentsStripeFormSavedCard";
import ApolloClient from "apollo-client";

const StripeWrapper = styled.div`
margin: 20px 0 0;
max-width: 500px;
width: 100%;
`;
import { PaymentsStripeCardComponent } from "./PaymentsStripeCardComponent";

export interface PaymentIntentsStripeFormProps extends ReactStripeElements.InjectedStripeProps {
postId: string;
Expand Down Expand Up @@ -147,42 +134,16 @@ class PaymentIntentsStripeForm extends React.Component<PaymentIntentsStripeFormP
)}
{showCreditCardForm && (
<>
<StripeWrapper>
{this.state.wasEmailPrepopulated && <PaymentEmailPrepopulatedText email={this.state.email} />}
{!this.state.wasEmailPrepopulated && (
<>
<PaymentInputLabel>Email</PaymentInputLabel>
<InputValidationUI inputState={this.state.emailState}>
<input
defaultValue={this.state.email}
id="email"
name="email"
type="email"
maxLength={254}
onBlur={() => this.handleOnBlur(event)}
/>
<PaymentEmailConfirmationText />
</InputValidationUI>
</>
)}
<PaymentInputLabel>Name on card</PaymentInputLabel>
<InputValidationUI inputState={this.state.nameState}>
<input id="name" name="name" onBlur={() => this.handleOnBlur(event)} required />
</InputValidationUI>
<PaymentInputLabel>Card information</PaymentInputLabel>
<InputStripeValidationUI inputState={this.state.cardInfoState}>
<StripeElement inputState={this.state.cardInfoState}>
<CardElement
id="card-info"
style={{ base: { fontSize: "13px" } }}
onChange={this.handleStripeChange}
/>
</StripeElement>
{this.state.displayStripeErrorMessage !== "" && (
<InputErrorMessage>{this.state.displayStripeErrorMessage}</InputErrorMessage>
)}
</InputStripeValidationUI>
</StripeWrapper>
<PaymentsStripeCardComponent
email={this.state.email}
wasEmailPrepopulated={this.state.wasEmailPrepopulated}
emailState={this.state.emailState}
nameState={this.state.nameState}
cardInfoState={this.state.cardInfoState}
displayStripeErrorMessage={this.state.displayStripeErrorMessage}
handleOnBlur={this.handleOnBlur}
handleStripeChange={this.handleStripeChange}
/>
{this.props.userChannelID && this.props.userChannelID !== "" && (
<>
<PaymentInputLabel>Remember Credit Card</PaymentInputLabel>
Expand Down
67 changes: 67 additions & 0 deletions packages/components/src/Payments/PaymentsStripeCardComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from "react";
import styled from "styled-components";
import { PaymentInputLabel } from "./PaymentsStyledComponents";
import { InputValidationUI, InputStripeValidationUI, StripeElement, InputErrorMessage } from "./PaymentsInputValidationUI";
import { PaymentEmailPrepopulatedText, PaymentEmailConfirmationText, AddCardEmailConfirmationText } from "./PaymentsTextComponents";
import { CardElement } from "react-stripe-elements";

const StripeWrapper = styled.div`
margin: 20px 0 0;
max-width: 500px;
width: 100%;
`;

interface PaymentsStripeCardComponentProps {
email: string;
wasEmailPrepopulated: boolean;
emailState: string;
nameState: string;
cardInfoState: string;
displayStripeErrorMessage: string;
showAddCardText?: boolean;
handleOnBlur(event: any): void;
handleStripeChange(event: any): void;
}

export const PaymentsStripeCardComponent: React.FunctionComponent<PaymentsStripeCardComponentProps> = props => {
const { email, wasEmailPrepopulated, emailState, nameState, cardInfoState, displayStripeErrorMessage, handleOnBlur, handleStripeChange, showAddCardText } = props;
return (
<StripeWrapper>
{wasEmailPrepopulated && <PaymentEmailPrepopulatedText email={email} />}
{!wasEmailPrepopulated && (
<>
<PaymentInputLabel>Email</PaymentInputLabel>
<InputValidationUI inputState={emailState}>
<input
defaultValue={email}
id="email"
name="email"
type="email"
maxLength={254}
onBlur={() => handleOnBlur(event)}
/>
{showAddCardText && <AddCardEmailConfirmationText />}
{!showAddCardText && <PaymentEmailConfirmationText />}
</InputValidationUI>
</>
)}
<PaymentInputLabel>Name on card</PaymentInputLabel>
<InputValidationUI inputState={nameState}>
<input id="name" name="name" onBlur={() => handleOnBlur(event)} required />
</InputValidationUI>
<PaymentInputLabel>Card information</PaymentInputLabel>
<InputStripeValidationUI inputState={cardInfoState}>
<StripeElement inputState={cardInfoState}>
<CardElement
id="card-info"
style={{ base: { fontSize: "13px" } }}
onChange={handleStripeChange}
/>
</StripeElement>
{displayStripeErrorMessage !== "" && (
<InputErrorMessage>{displayStripeErrorMessage}</InputErrorMessage>
)}
</InputStripeValidationUI>
</StripeWrapper>
);
}
4 changes: 4 additions & 0 deletions packages/components/src/Payments/PaymentsTextComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export const PaymentEmailConfirmationText: React.FunctionComponent = props => (
<p>We’ll be sending you a confirmation email of your completed transaction.</p>
);

export const AddCardEmailConfirmationText: React.FunctionComponent = props => (
<p>We’ll keep your email on file to send confirmations of completed transactions.</p>
);

export const PaymentTermsText: React.FunctionComponent = props => (
<>
By sending a Boost, you agree to Civil’s{" "}
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/Payments/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./AvatarLogin";
export * from "./Payments";
export * from "./PaymentsModal";
export * from "./PaymentsStripeCardComponent";
2 changes: 2 additions & 0 deletions packages/dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/sanitize-html": "^1.14.0",
"@types/storejs": "^2.0.2",
"apollo-boost": "^0.1.16",
"apollo-client": "^2.6.8",
"connected-react-router": "^6.3.1",
"diff": "^4.0.1",
"graphql": "^14.0.2",
Expand All @@ -35,6 +36,7 @@
"json-loader": "^0.5.7",
"react": "^16.11.0",
"react-apollo": "^2.3.3",
"react-async-script": "^1.1.1",
"react-dom": "^16.11.0",
"react-ellipsis-text": "^1.2.1",
"react-helmet": "^5.2.0",
Expand Down
163 changes: 163 additions & 0 deletions packages/dapp/src/components/Dashboard/Account/AccountAddCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import * as React from "react";
import { PaymentsStripeCardComponent, Button, buttonSizes, InvertedButton } from "@joincivil/components";
import { isValidEmail } from "@joincivil/utils";
import { injectStripe, ReactStripeElements } from "react-stripe-elements";
import ApolloClient from "apollo-client";
import { CREATE_PAYMENT_METHOD } from "@joincivil/components/src/Payments/queries";
import styled from "styled-components";

export interface AccountAddCardProps extends ReactStripeElements.InjectedStripeProps {
userEmail: string;
userChannelID: string;
apolloClient: ApolloClient<any>;
handleCancel(): void;
handleAdded(): void;
}

export interface AccountAddCardState {
email: string;
emailState: string;
name: string;
nameState: string;
cardInfoState: string;
wasEmailPrepopulated: boolean;
displayStripeErrorMessage: string;
addCardDisabled: boolean;
}

enum INPUT_STATE {
EMPTY = "empty",
VALID = "valid",
INVALID = "invalid",
}

const ButtonDiv = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
`;

const SaveButton = styled(Button)`
margin-left: 10px;
`;

class AccountAddCard extends React.Component<AccountAddCardProps, AccountAddCardState> {

constructor(props: AccountAddCardProps) {
super(props);
this.state = {
email: props.userEmail || "",
wasEmailPrepopulated: props.userEmail ? true : false,
emailState: this.props.userEmail ? INPUT_STATE.VALID : INPUT_STATE.EMPTY,
name: "",
nameState: INPUT_STATE.EMPTY,
cardInfoState: INPUT_STATE.EMPTY,
displayStripeErrorMessage: "",
addCardDisabled: false,
};
}

public render(): JSX.Element {
return (
<div>
<PaymentsStripeCardComponent
email={this.state.email}
emailState={this.state.emailState}
name={this.state.name}
nameState={this.state.nameState}
cardInfoState={this.state.cardInfoState}
wasEmailPrepopulated={this.state.wasEmailPrepopulated}
displayStripeErrorMessage={this.state.displayStripeErrorMessage}
handleOnBlur={this.handleOnBlur}
handleStripeChange={this.handleStripeChange}
showAddCardText={true}
/>
<ButtonDiv>
<InvertedButton onClick={this.props.handleCancel} size={buttonSizes.SMALL}>Cancel</InvertedButton>
<SaveButton onClick={this.handleAddCard} disabled={this.state.addCardDisabled} size={buttonSizes.SMALL}>Save</SaveButton>
</ButtonDiv>
</div>
);
}

private handleAddCard = async () => {
try {
this.setState({addCardDisabled: true});
const result = await (this.props.stripe as any).createPaymentMethod({
type: "card",
card: (this.props as any).elements.getElement("card"),
billing_details: {
name: this.state.name,
email: this.state.email,
},
});

const paymentMethodID = result.paymentMethod.id;

const paymentMethodVariables = {
input: {
paymentMethodID,
emailAddress: this.state.email,
payerChannelID: this.props.userChannelID,
},
};

const paymentMethodResult = await this.props.apolloClient.mutate({
mutation: CREATE_PAYMENT_METHOD,
variables: paymentMethodVariables,
});
if (paymentMethodResult.error) {
console.error(paymentMethodResult.error);
this.setState({ addCardDisabled: false });
}
this.props.handleAdded();
this.setState({addCardDisabled: false});
} catch (err) {
this.setState({addCardDisabled: false});
}
}

private handleOnBlur = (event: any) => {
const state = event.target.id;
const value = event.target.value;

switch (state) {
case "email":
const validEmail = isValidEmail(value);
validEmail
? this.setState({ email: value, emailState: INPUT_STATE.VALID })
: this.setState({ emailState: INPUT_STATE.INVALID });
break;
case "name":
const validName = value !== "";
validName
? this.setState({ name: value, nameState: INPUT_STATE.VALID })
: this.setState({ nameState: INPUT_STATE.INVALID });
break;
default:
break;
}
};

private handleStripeChange = (event: any) => {
const stripeElements = document.querySelectorAll(".StripeElement");
let displayStripeErrorMessage = "";

if (event.error) {
displayStripeErrorMessage = event.error.message;
}

stripeElements.forEach(element => {
const classList = element.classList;
if (classList.contains("StripeElement--invalid")) {
this.setState({ cardInfoState: INPUT_STATE.INVALID, displayStripeErrorMessage });
} else if (classList.contains("StripeElement--empty")) {
this.setState({ cardInfoState: INPUT_STATE.EMPTY, displayStripeErrorMessage });
} else {
this.setState({ cardInfoState: INPUT_STATE.VALID, displayStripeErrorMessage });
}
});
};
}

export default injectStripe(AccountAddCard);
Loading

0 comments on commit a865b4d

Please sign in to comment.