Sun Nov 20 2022
Copied to clipboard! Copy reply
  • 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,
 };