- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
/**
* This component holds a full Credit Card form.
* When submitted, it will validate its input fields.
* If they all validate, then it calls a function passe in as a prop,
* `submitCallback` passed in by parent component.
* There is an optional `cancel` button - if you wish to use it, just pass
* `cancelButtonText`and `cancelButtonCallback` props and the form knows there's a cancel button
* to show. If you omit those props, no cancel button will appear.
*/
import styled from "@emotion/styled";
import Lion from "@rosetta/react-lion";
import PropTypes from "prop-types";
import React from "react";
import { Tooltip } from "../common/components/Tooltip";
//import { usStates } from "../common/util/us-states";
import { errorsMap } from "./errors";
const usStates = [
{ name: 'ALABAMA', abbreviation: 'AL'},
{ name: 'ALASKA', abbreviation: 'AK'},
{ name: 'AMERICAN SAMOA', abbreviation: 'AS'},
{ name: 'ARIZONA', abbreviation: 'AZ'},
{ name: 'ARKANSAS', abbreviation: 'AR'},
{ name: 'CALIFORNIA', abbreviation: 'CA'},
{ name: 'COLORADO', abbreviation: 'CO'},
{ name: 'CONNECTICUT', abbreviation: 'CT'},
{ name: 'DELAWARE', abbreviation: 'DE'},
{ name: 'DISTRICT OF COLUMBIA', abbreviation: 'DC'},
{ name: 'FEDERATED STATES OF MICRONESIA', abbreviation: 'FM'},
{ name: 'FLORIDA', abbreviation: 'FL'},
{ name: 'GEORGIA', abbreviation: 'GA'},
{ name: 'GUAM', abbreviation: 'GU'},
{ name: 'HAWAII', abbreviation: 'HI'},
{ name: 'IDAHO', abbreviation: 'ID'},
{ name: 'ILLINOIS', abbreviation: 'IL'},
{ name: 'INDIANA', abbreviation: 'IN'},
{ name: 'IOWA', abbreviation: 'IA'},
{ name: 'KANSAS', abbreviation: 'KS'},
{ name: 'KENTUCKY', abbreviation: 'KY'},
{ name: 'LOUISIANA', abbreviation: 'LA'},
{ name: 'MAINE', abbreviation: 'ME'},
{ name: 'MARSHALL ISLANDS', abbreviation: 'MH'},
{ name: 'MARYLAND', abbreviation: 'MD'},
{ name: 'MASSACHUSETTS', abbreviation: 'MA'},
{ name: 'MICHIGAN', abbreviation: 'MI'},
{ name: 'MINNESOTA', abbreviation: 'MN'},
{ name: 'MISSISSIPPI', abbreviation: 'MS'},
{ name: 'MISSOURI', abbreviation: 'MO'},
{ name: 'MONTANA', abbreviation: 'MT'},
{ name: 'NEBRASKA', abbreviation: 'NE'},
{ name: 'NEVADA', abbreviation: 'NV'},
{ name: 'NEW HAMPSHIRE', abbreviation: 'NH'},
{ name: 'NEW JERSEY', abbreviation: 'NJ'},
{ name: 'NEW MEXICO', abbreviation: 'NM'},
{ name: 'NEW YORK', abbreviation: 'NY'},
{ name: 'NORTH CAROLINA', abbreviation: 'NC'},
{ name: 'NORTH DAKOTA', abbreviation: 'ND'},
{ name: 'NORTHERN MARIANA ISLANDS', abbreviation: 'MP'},
{ name: 'OHIO', abbreviation: 'OH'},
{ name: 'OKLAHOMA', abbreviation: 'OK'},
{ name: 'OREGON', abbreviation: 'OR'},
{ name: 'PALAU', abbreviation: 'PW'},
{ name: 'PENNSYLVANIA', abbreviation: 'PA'},
{ name: 'PUERTO RICO', abbreviation: 'PR'},
{ name: 'RHODE ISLAND', abbreviation: 'RI'},
{ name: 'SOUTH CAROLINA', abbreviation: 'SC'},
{ name: 'SOUTH DAKOTA', abbreviation: 'SD'},
{ name: 'TENNESSEE', abbreviation: 'TN'},
{ name: 'TEXAS', abbreviation: 'TX'},
{ name: 'UTAH', abbreviation: 'UT'},
{ name: 'VERMONT', abbreviation: 'VT'},
{ name: 'VIRGIN ISLANDS', abbreviation: 'VI'},
{ name: 'VIRGINIA', abbreviation: 'VA'},
{ name: 'WASHINGTON', abbreviation: 'WA'},
{ name: 'WEST VIRGINIA', abbreviation: 'WV'},
{ name: 'WISCONSIN', abbreviation: 'WI'},
{ name: 'WYOMING', abbreviation: 'WY' }
];
const Form = styled.form({
"& > div": {
margin: "20px 0",
fontFamily: "helvetica",
color: "#262626",
fontSize: "20px",
textAlign: "left",
lineHeight: "23px",
"&::last-of-type": {
marginBottom: 0,
}
},
label: {
width: "100%",
},
"label > div": {
display: "block",
fontWeight: 700,
marginTop: "4px",
marginBottom: "9px",
},
"input, select": {
display: "block",
width: "100%",
padding: "13px 6px",
borderRadius: "3px",
border: "1px solid #999",
boxSizing: "border-box",
fontSize: "20px;"
},
select: {
//padding: "12px 5px",
//textAlign: "center"
},
".wrap-row": {
margin: "15px 0",
display: "flex",
"& > label": {
marginRight: "21px",
"&:last-of-type": {
marginRight: 0,
}
}
},
".expiration > div": {
display: "flex",
select: {
width: "100px"
}
},
".slash-divider": {
display: "flex",
alignItems: "center",
padding: "0 12px",
fontWeight: 100,
fontSize: "18px"
},
"input[type='submit'], input[type='button']": {
background: "#02B1FF",
color: "#fff",
border: "1px solid transparent",
borderRadius: "10px",
padding: "15px 0",
fontSize: "17px",
textTransform: "uppercase",
"&:disabled": {
background: '#ccc'
},
"&.cancel": {
background: "#999",
color: "#fff",
border: "1px solid #999",
marginLeft: "10px"
}
},
".support-info": {
fontSize: "16px",
lineHeight: "24px",
textAlign: "center",
color: "#262626",
fontWeight: 400,
maxWidth: "450px",
borderTop: "1px solid #e1e1e1",
paddingTop: "21px",
marginTop: "24px"
},
".form-error": {
background: "#ffbeca",
border: "3px solid #e25454",
padding: "5px",
fontSize: "16px",
lineHeight: "23px",
},
".cvv-container": {
display: "flex",
alignItems: "center",
"svg": {
width: "39px",
height: "39px",
marginLeft: "8px"
}
}
})
// Make sure mo/yr is not in past
function validateExpirationDate(dropdownMonth, dropdownYear) {
let ret = true
if (!dropdownMonth && !dropdownYear) {
ret = {msg: "Expiration month and year cannot be blank."}
} else if(!dropdownMonth) {
ret = {msg: "Expiration month cannot be blank."}
} else if (!dropdownYear) {
ret = {msg: "Expiration year cannot be blank."}
}
const currentMonth = new Date().getMonth() + 1
const currentYear = parseInt(new Date().getFullYear().toString().slice(-2))
if (dropdownMonth < currentMonth && dropdownYear === currentYear) {
ret = {msg: "Expiration date cannot be in the past."}
}
return ret
}
// For Credit Card expiration year dropdown
const expirationYears = [""]
const currentYear = new Date().getFullYear()
for (let year = currentYear; year < currentYear + 15; year++) {
expirationYears.push({fullYear: year, shortYear: year.toString().slice(2)})
}
export class CreditCardForm extends React.Component {
constructor(props) {
super(props);
this.props = props
console.log('------')
console.log(this.props)
this.state = {
maskedCreditCardNumber: null,
cardName: "",
cardBillingAddress: "",
cardBillingAddress2: "",
cardNumber: "",
cardState: "",
cardZip: "",
cardCity: "",
cardExpirationMonth: "",
cardExpirationYear: "",
cardSecurityCode: "",
country: "US",
isFetching: false,
errors: [],
submitButtonText: "SUBMIT",
cancelButtonText: "CANCEL",
submitCallback: null,
}
}
// If an `error` prop is passed, add it to `errors` state
componentDidUpdate(prevProps) {
// Check for prevProps to avoid maximum update depth error
if (this.props.error && (this.props.error !== prevProps.error)) {
this.setState({errors: [this.props.error]})
}
}
handleOnBlur(e) {
if (e.target.value === "" && this.state.maskedCreditCard) {
this.setState({ cardNumber: this.state.maskedCreditCard});
}
}
handleOnFocus(e) {
if (e.target.value.indexOf('*') !== -1) {
// Store this value. If user focuses out without changes, we
// restore this value in `handleOnBlur`
this.setState({
maskedCreditCardNumber: e.target.value,
cardNumber: "",
});
}
}
handleOnSubmit = (e) => {
e.preventDefault()
console.log(this.state)
// Number validation
if (
!this.state.cardName
|| this.state.cardName.trim() > 100
|| this.state.cardName.trim() < 2
|| !/^([a-z]+(-|\.| )*)+$/i.test(this.state.cardName))
{
this.state.errors.push("Name is invalid.")
}
// Card number validation
if (
!(this.state.cardNumber.length >= 15
&& this.state.cardNumber.length <= 16)
|| /\*/.test(this.state.cardNumber))
{
this.state.errors.push(errorsMap["InvalidCCNumber"])
}
// Zip code validation
if (!this.state.cardZip || this.state.cardZip.length !== 5 ) {
this.state.errors.push("Zip code is invalid.")
}
// Expiration date can't be in past
const isExpDateValid = validateExpirationDate(
parseInt(this.state.cardExpirationMonth),
parseInt(this.state.cardExpirationYear)
)
if (isExpDateValid !== true) {
this.state.errors.push(isExpDateValid.msg)
}
// Security code validation - 3 or 4 digits
if (
!this.state.cardSecurityCode
|| !(this.state.cardSecurityCode.length >= 3
&& this.state.cardSecurityCode.length <= 4))
{
this.state.errors.push("Security code is invalid.")
}
// Validation passed. Make API calls on parent component
if (this.state.errors.length) {
return
}
// No errors. Call parent's handle function
this.setState({errors: []})
this.props.submitCallback(this.state)
}
render() {
return(
<Form onSubmit={this.handleOnSubmit}>
{/* Full Name */}
<div>
<label>
<Lion.div lionkey="full_name" />
<input
type="text"
minLength="2"
maxLength="100"
defaultValue={this.props.cardName}
onChange={e => this.setState({ cardName: e.target.value })}
/>
</label>
</div>
{/* Address 1 */}
<div>
<label>
<div>Billing Address</div>
<input
type="text"
minLength="2"
maxLength="100"
defaultValue={this.state.cardBillingAddress}
onChange={e => this.setState({ cardName: e.target.value })}
/>
</label>
</div>
{/* Address 2 */}
<div>
<label>
<div>Billing Address 2</div>
<input
type="text"
minLength="2"
maxLength="100"
defaultValue={this.state.cardBillingAddress2}
onChange={e => this.setState({ cardName: e.target.value })}
/>
</label>
</div>
{/* Country & State */}
<div className="wrap-row">
<label className="country">
<div>Country</div>
<select value={this.state.cardCountry || ""}
onChange={e => this.setState({ crdCountry: e.target.value }) }>
{
usStates.map(state =>
<option
key={state.abbreviation}
value={state.abbreviation}>
{state.abbreviation}
</option>
)
}
</select>
</label>
<label className="state">
<Lion.div lionkey="state" />
<select value={this.state.cardState || ""}
onChange={e => this.setState({ cardState: e.target.value }) }>
{
usStates.map(state =>
<option
key={state.abbreviation}
value={state.abbreviation}>
{state.abbreviation}
</option>
)
}
</select>
</label>
</div>
{/* City & Zip */}
<div className="wrap-row">
<label className="city">
<div>City</div>
<input
minLength="5"
maxLength="5"
type="text"
defaultValue={this.state.city}
onInput={e => this.setState({ cardCity: e.target.value })}
/>
</label>
<label className="zip">
<Lion.div lionkey="zip_code" />
<input
minLength="5"
maxLength="5"
type="text"
defaultValue={this.state.cardZip}
onInput={e =>{
e.target.value = e.target.value.replace(/[^0-9]/g, '');
this.setState({ cardZip: e.target.value })
}}
/>
</label>
</div>
{/* Number */}
<div>
<label className="card-number">
<Lion.div lionkey="card_number" />
<input
type="text"
onFocus={e => this.handleOnFocus(e)}
onBlur={e => this.handleOnBlur(e)}
defaultValue={this.state.cardNumber}
minLength="15"
maxLength="16"
onInput={e =>{
e.target.value = e.target.value.replace(/[^0-9]/g, '');
this.setState({ cardNumber: e.target.value })
}}
/>
</label>
</div>
{/* Expiration Date, Security code */}
<div className="wrap-row">
<label className="expiration">
<Lion.div lionkey="expiration_date" />
<div>
<select
value={this.state.cardExpirationMonth}
onChange={e => this.setState({ cardExpirationMonth: e.target.value })}>
{
["", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]
.map(month => <option key={parseInt(month)}>{ month }</option>)
}
</select>
<span className="slash-divider">/</span>
<select
value={this.state.cardExpirationYear}
onChange={e => this.setState({ cardExpirationYear: e.target.value }) }>
{
expirationYears.map(year => <option key={parseInt(year.shortYear)}>
{ year.shortYear }
</option>)
}
</select>
</div>
</label>
<label className="security-code">
<Lion.div lionkey="security_code" />
<div className="cvv-container">
<input
type="text"
maxLength="4"
minLength="3"
onInput={e =>this.setState({ cardSecurityCode: e.target.value })}
/>
<Tooltip title={this.props.tooltipText} position="topleft">
<svg title="cvv-tooltip" width="39" height="39" viewBox="0 0 39 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="19.5" cy="19.5" r="19.5" fill="#02B1FF"/>
<path d="M17.9023 24.1484C17.9023 23.5781 17.9375 23.1016 18.0078 22.7188C18.0781 22.3281 18.1914 21.9844 18.3477 21.6875C18.5117 21.3906 18.7422 21.0977 19.0391 20.8086C19.3359 20.5195 19.7188 20.1914 20.1875 19.8242C20.5938 19.4961 20.9297 19.1562 21.1953 18.8047C21.4609 18.4531 21.6641 18.0898 21.8047 17.7148C21.9453 17.3398 22.0156 16.9531 22.0156 16.5547C22.0156 15.8828 21.8281 15.3594 21.4531 14.9844C21.0859 14.6016 20.5664 14.4102 19.8945 14.4102C19.332 14.4102 18.8477 14.5664 18.4414 14.8789C18.043 15.1914 17.8398 15.6602 17.832 16.2852H14.5156L14.4805 16.2148C14.4648 15.2617 14.6836 14.4492 15.1367 13.7773C15.5977 13.0977 16.2344 12.582 17.0469 12.2305C17.8672 11.8711 18.8125 11.6914 19.8828 11.6914C21.625 11.6914 22.9844 12.1211 23.9609 12.9805C24.9453 13.832 25.4375 14.9922 25.4375 16.4609C25.4375 17.1406 25.3047 17.7695 25.0391 18.3477C24.7812 18.9258 24.4219 19.4727 23.9609 19.9883C23.5078 20.5039 22.9805 21.0156 22.3789 21.5234C22.082 21.7734 21.8555 22.0156 21.6992 22.25C21.5508 22.4766 21.4492 22.7422 21.3945 23.0469C21.3477 23.3438 21.3242 23.7109 21.3242 24.1484H17.9023ZM17.9023 29V26H21.3242V29H17.9023Z" fill="white"/>
</svg>
</Tooltip>
</div>
</label>
</div>
<div className="wrap-row">
<input type="submit" value={this.props.submitButtonText} disabled={this.state.isFetching} />
<input type="button" className="cancel" value="cancel" onClick={this.props.cancelButtonCallback} />
</div>
{/*
Possible anti pattern here, but not only will we display local/validation errors,
we'll also display any XHR errors in the redux store - this is because the parent component
is making API calls, and those calls will store errors in the redux store.
*/}
{ this.state.errors.length > 0 &&
<div className="form-error">
{ this.state.errors.map((e, i) =>
<Lion.div key={i} lionkey={e} />
)}
</div>
}
</Form>
)
};
}
CreditCardForm.propTypes = {
/* the text string value of the submit button */
submitButtonText: PropTypes.string,
/* text string for cancel button. NOTE: if not passed, button will not show */
cancelButtonText: PropTypes.string,
/* Function defined in parent component, called if input validations pass. */
submitCallback: PropTypes.func,
/* Function defined in parent component, called when cancel button is clicked. */
cancelButtonCallback: PropTypes.func,
};