react-hcaptcha icon indicating copy to clipboard operation
react-hcaptcha copied to clipboard

Call execute after load if execute was called before ready

Open ZhangYiJiang opened this issue 3 years ago • 10 comments

The component has a race condition where if execute() is called before the API is loaded, execute will just fail silently. The fix is to add a flag which we check on load, and calls execute again once the API is loaded if the parent had previously called it. The caller could implement the same fix we are implementing here but it is easier if the component just took care of it automatically. This PR is not compatible with https://github.com/hCaptcha/react-hcaptcha/pull/91

ZhangYiJiang avatar Sep 07 '21 22:09 ZhangYiJiang

Why not use onLoad and call execute then?

brdlyptrs avatar Sep 08 '21 18:09 brdlyptrs

Do you mean the parent can call execute in onLoad? They absolutely can, but like I said it seems surprising that the component doesn't do this already, and it's very unlikely the caller would not want this to happen.

ZhangYiJiang avatar Sep 09 '21 02:09 ZhangYiJiang

@ZhangYiJiang could you provide tests here to illustrate what is expected?

brdlyptrs avatar Sep 09 '21 20:09 brdlyptrs

Sure, added a test case that fails on master. It's a bit weird because we need the global hcaptcha not to be defined when the component is mounted to simulate the script loading after the component is mounted, so this is the best I can do

 FAIL  tests/hcaptcha.spec.js
  ● hCaptcha › correctly executes even if execute() was called before load

    expect(received).toBe(expected) // Object.is equality

    Expected: 1
    Received: 0

      163 |         window.hcaptcha = hcaptchaGlobal;
      164 |         instance.handleOnLoad();
    > 165 |         expect(hcaptchaGlobal.execute.mock.calls.length).toBe(1);
          |                                                          ^
      166 |     })
      167 | 
      168 |     describe("Query parameter", () => {

      at Object.<anonymous> (tests/hcaptcha.spec.js:165:58)

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 30 passed, 31 total
Snapshots:   0 total
Time:        4.669 s
Ran all test suites.

ZhangYiJiang avatar Sep 13 '21 23:09 ZhangYiJiang

I'm done with #91, and I don't see why it shouldn't be compatible now.

ajmnz avatar Sep 22 '21 23:09 ajmnz

https://github.com/hCaptcha/react-hcaptcha/pull/91/files#diff-bfe9874d239014961b1ae4e89875a6155667db834a410aaaa2ebe3cf89820556R210

This returns Promise<undefined> if !isApiReady - you would expect this to always return a promise resolving to the token, even if the API is not loaded (similar how execute() works after this change).

Fixing this is a bit tricky - you'll probably need something like this

executeAsync() {
  if (!isApiReady) {
    if (this.executePromise == null) {
      this.executePromise = new Promise((resolve) => {
        this.resolveExecutePromise = resolve;
      });
    }
    // This means all calls before isApiReady will resolve to the same token -
    // I assume this is okay, but if it is not we can store an array of promises instead
    return this.executePromise;
  }

  // ...
}

handleOnLoad() {
  // ...
  if (this.resolveExecutePromise != null) {
    resolveExecutePromise(this.executeAsync());
  }
}

Also FWIW given that the only place async is used is here https://github.com/hCaptcha/react-hcaptcha/pull/91/files#diff-bfe9874d239014961b1ae4e89875a6155667db834a410aaaa2ebe3cf89820556R207, which just returns a promise directly, it's might be better to just not add regenerator

ZhangYiJiang avatar Sep 23 '21 00:09 ZhangYiJiang

@ZhangYiJiang You could create a new promise that is returned and then when execute does get called you can resolve when that actual promise is resolved.

brdlyptrs avatar Sep 27 '21 21:09 brdlyptrs

@ZhangYiJiang should we still try to implement this PR, what do you think?

brdlyptrs avatar Oct 27 '21 23:10 brdlyptrs

Hi @ZhangYiJiang looks like I encountered a similar issue as you are trying to solve here. Can you have a look and see if this PR solves the one here?

brdlyptrs avatar Nov 04 '21 23:11 brdlyptrs

No that's not quite the same issue - this is calling execute before load, that is calling execute on load. That said, since #91 is merged, this also needs to handle the async case. If execute() is called multiple times, should the same promise be returned, ie. all execute() calls resolve to the same token?

ZhangYiJiang avatar Nov 09 '21 04:11 ZhangYiJiang