javascriptreactjsiframecredit-cardcybersource

Why IFrame does not load on a single click on container div?


Background:

Working on Cybersource Credit Card integration with React app. Need to show Masked card number coming from API response upon tabbing out (onBlur) of field.

Flow

  1. Created a container-div in which IFrame gets loaded and I enter CC number. On blur event, doing API calls for validation and getting the masked card number (if successfully validated).

  2. Upon setting maskedCardNumber, component gets re-rendered and shows the maskedCardNumber in the container-div. At this point I see in Elements tab, IFrame is gone which is perfectly fine to me. Now in my container div there is no IFrame but a masked card number.

I want to change CC number

  1. I click on input alike div and masked card number gets removed because of setMaskedCardNumber("");. ( Not each character one by one but all in once as we do not have that card number). Till this point everything is fine.

Problem

After removal, it should show new Iframe because I am calling loadIFrame(); like I did for initial Iframe loading but weirdly it does not show until I click AGAIN. YES!! you read it right. I need to click AGAIN to load and IFrame which is very weird for me.

Tried so far

  1. useState(),
  2. useReducer(),
  3. to make <label>{maskedCardNumber}</label> inside container-div.
  4. Changing conatiner-div <div> to <input>

Relevant code

  useEffect(() => {
    if (apiKey) {
      loadIFrame();
    }
  }, [apiKey])


const loadIFrame = () => {
let flex;
flex = new Flex(apiKey);
let microform = flex.microform();
let number = microform.createField('number', {
    placeholder: 'Enter card number'
});
number.on('load', () => {
    number.focus();
});
number.on('blur', () => {
  
    //transient token call
    microform.createToken({}, function(err, token) {
        if (err) {
            console.error(err);
            setCardError({
                ...error,
                token: "Please enter valid CC number"
            })
        } else {
            setCardError({
                ...error,
                token: ""
            });

            //permenant token call
            setMaskedCardNumber(JSON.parse(atob(token.split('.')[1]))["data"]["number"]);
            dispatch({
                type: orderActions.GET_CC_TOKEN,
                transientToken: JSON.parse(atob(token.split('.')[1]))['jti'],
                callbacks: {
                    success: (ccToken) => {
                        updateCardDetail({
                            type: "token",
                            token: ccToken
                        })
                    },
                    failure: (err) => {
                        console.error(err);
                        setCardError({
                            ...error,
                            token: "Please enter valid CC number"
                        })
                    }
                }
            })
        }
    })
});
number.load('#number-container');
}

    <div id="number-container" className="form-control" onClick={() => {
        setMaskedCardNumber("");
        loadIFrame();
      }}>
        {maskedCardNumber}
   </div>

Solution

  • Problem I see here is basically the response to your setMaskedCardNumber() call returns after the iFrame is loaded due to its async nature.

    Try putting this effect

      useEffect(()=>{
        if(maskedCardNumber === ""){
          loadIFrame()
        }
      }, [maskedCardNumber])
    

    and remove loadIFrame(); from onClick() handler

    this will make sure whenever you empty the cardNumber state, iFrame is loaded when it is successfully emptied.

    UPDATE:

    We can make use of single useEffect.

      useEffect(() => {
        if (apiKey && !maskedCardNumber) {
          loadIFrame();
        }
      }, [apiKey, maskedCardNumber])
    

    With this we can make sure to load IFrame only if apiKey present otherwise do not try to load because IFrame is dependant on apiKey.